add revision number

This commit is contained in:
Aditya Siregar 2025-10-13 08:46:26 +07:00
parent 4e58ce95fb
commit e58472c963
13 changed files with 364 additions and 48 deletions

View File

@ -107,6 +107,7 @@ type OutgoingLetterResponse struct {
IssueDate time.Time `json:"issue_date"`
Status string `json:"status"`
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
RevisionNumber int `json:"revision_number"`
CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@ -157,6 +158,12 @@ type RejectLetterRequest struct {
Reason string `json:"reason" validate:"required"`
}
type ReviseLetterRequest struct {
FileURL string `json:"file_url" validate:"required"`
FileName string `json:"file_name" validate:"required"`
FileType string `json:"file_type" validate:"required"`
}
type AddRecipientsRequest struct {
Recipients []CreateOutgoingLetterRecipient `json:"recipients" validate:"required,dive"`
}

View File

@ -49,17 +49,18 @@ const (
)
type LetterOutgoingApproval 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"`
StepID uuid.UUID `gorm:"type:uuid;not null" json:"step_id"`
StepOrder int `gorm:"not null" json:"step_order"`
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
IsRequired bool `gorm:"default:true" json:"is_required"`
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
Status ApprovalStatus `gorm:"not null;default:'pending'" json:"status"`
Remarks *string `json:"remarks,omitempty"`
ActedAt *time.Time `json:"acted_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
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"`
StepID uuid.UUID `gorm:"type:uuid;not null" json:"step_id"`
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
StepOrder int `gorm:"not null" json:"step_order"`
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
IsRequired bool `gorm:"default:true" json:"is_required"`
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
Status ApprovalStatus `gorm:"not null;default:'pending'" json:"status"`
Remarks *string `json:"remarks,omitempty"`
ActedAt *time.Time `json:"acted_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// Relations
Letter *LetterOutgoing `gorm:"foreignKey:LetterID" json:"letter,omitempty"`

View File

@ -28,6 +28,7 @@ type LetterOutgoing struct {
IssueDate time.Time `json:"issue_date"`
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
IsArchived bool `gorm:"not null;default:false" json:"is_archived"`
ArchivedAt *time.Time `json:"archived_at,omitempty"`
@ -67,13 +68,14 @@ type LetterOutgoingRecipient struct {
func (LetterOutgoingRecipient) TableName() string { return "letter_outgoing_recipients" }
type LetterOutgoingAttachment 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"`
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"`
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
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 (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" }

View File

@ -36,6 +36,7 @@ const (
LetterOutgoingActionSubmittedApproval = "submitted_for_approval"
LetterOutgoingActionApproved = "approved"
LetterOutgoingActionRejected = "rejected"
LetterOutgoingActionRevised = "revised"
LetterOutgoingActionSent = "sent"
LetterOutgoingActionArchived = "archived"
LetterOutgoingActionAttachmentAdded = "attachment_added"

View File

@ -23,6 +23,7 @@ type LetterOutgoingService interface {
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error
ReviseOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ReviseLetterRequest) error
SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
@ -217,6 +218,27 @@ func (h *LetterOutgoingHandler) RejectOutgoingLetter(c *gin.Context) {
c.JSON(http.StatusOK, contract.BuildSuccessResponse(&contract.SuccessResponse{Message: "rejected"}))
}
func (h *LetterOutgoingHandler) ReviseOutgoingLetter(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.ReviseLetterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
return
}
if err := h.svc.ReviseOutgoingLetter(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: "revised"}))
}
func (h *LetterOutgoingHandler) SendOutgoingLetter(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {

View File

@ -24,8 +24,9 @@ type LetterOutgoingProcessor interface {
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error
ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error
ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error
ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error
ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error
ProcessRevision(ctx context.Context, letterID uuid.UUID, attachment entities.LetterOutgoingAttachment, userID uuid.UUID) error
AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error
UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error
@ -40,6 +41,7 @@ type LetterOutgoingProcessor interface {
DeleteDiscussion(ctx context.Context, id uuid.UUID) error
GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error)
GetApprovalsByLetterAndRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) ([]entities.LetterOutgoingApproval, error)
GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error)
// GetOutgoingLetterWithDetails fetches letter with all related data
@ -397,6 +399,12 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont
}
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Get the letter to get the current revision number
letter, err := p.letterRepo.Get(txCtx, letterID)
if err != nil {
return err
}
// Find the minimum step order (first step)
minStepOrder := flow.Steps[0].StepOrder
for _, step := range flow.Steps {
@ -408,13 +416,14 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont
approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps))
for i, step := range flow.Steps {
approvals[i] = entities.LetterOutgoingApproval{
LetterID: letterID,
StepID: step.ID,
StepOrder: step.StepOrder,
ParallelGroup: step.ParallelGroup,
IsRequired: step.Required,
ApproverID: step.ApproverUserID,
Status: entities.ApprovalStatusPending,
LetterID: letterID,
StepID: step.ID,
RevisionNumber: letter.RevisionNumber,
StepOrder: step.StepOrder,
ParallelGroup: step.ParallelGroup,
IsRequired: step.Required,
ApproverID: step.ApproverUserID,
Status: entities.ApprovalStatusPending,
}
// Set status based on step order
@ -483,15 +492,15 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont
})
}
func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error {
func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Step 1: Update the approval record
if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil {
return err
}
// Step 2: Get all approvals and organize by step
approvalsByStep, err := p.getApprovalsByStep(txCtx, letterID)
// Step 2: Get all approvals FOR THE SAME REVISION and organize by step
approvalsByStep, err := p.getApprovalsByStepForRevision(txCtx, letterID, approval.RevisionNumber)
if err != nil {
return err
}
@ -499,12 +508,12 @@ func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, lette
// Step 3: Check if current step is completed
if p.isStepCompleted(approvalsByStep[approval.StepOrder]) {
// Step 4: Activate next step if exists
if err := p.activateNextStep(txCtx, letterID, approval.StepOrder, approvalsByStep); err != nil {
if err := p.activateNextStepForRevision(txCtx, letterID, approval.StepOrder, approval.RevisionNumber, approvalsByStep); err != nil {
return err
}
}
// Step 5: Check if all required approvals are completed
// Step 5: Check if all required approvals are completed FOR THIS REVISION
if p.areAllRequiredApprovalsCompleted(approvalsByStep) {
// Step 6: Update letter status to approved
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
@ -542,6 +551,24 @@ func (p *LetterOutgoingProcessorImpl) getApprovalsByStep(ctx context.Context, le
return approvalsByStep, nil
}
// getApprovalsByStepForRevision fetches approvals for a specific revision and organizes them by step order
func (p *LetterOutgoingProcessorImpl) getApprovalsByStepForRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) (map[int][]entities.LetterOutgoingApproval, error) {
allApprovals, err := p.approvalRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
for _, approval := range allApprovals {
// Only include approvals from the same revision
if approval.RevisionNumber == revisionNumber {
approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval)
}
}
return approvalsByStep, nil
}
// isStepCompleted checks if all required approvals in a step are approved
func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool {
for _, approval := range stepApprovals {
@ -582,6 +609,39 @@ func (p *LetterOutgoingProcessorImpl) activateNextStep(ctx context.Context, lett
return nil
}
// activateNextStepForRevision activates the next approval step for a specific revision
func (p *LetterOutgoingProcessorImpl) activateNextStepForRevision(ctx context.Context, letterID uuid.UUID, currentStepOrder int, revisionNumber int, approvalsByStep map[int][]entities.LetterOutgoingApproval) error {
nextStepOrder := currentStepOrder + 1
nextStepApprovals, exists := approvalsByStep[nextStepOrder]
if !exists {
return nil // No next step
}
// Get existing recipients to avoid duplicates
existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID)
if err != nil {
return err
}
// Process each approval in the next step (already filtered by revision in approvalsByStep)
for _, nextApproval := range nextStepApprovals {
// Only process if it's the same revision
if nextApproval.RevisionNumber == revisionNumber {
// Activate approval if not started
if err := p.activateApprovalIfNotStarted(ctx, &nextApproval); err != nil {
return err
}
// Add approver as recipient if not already exists
if err := p.addApproverAsRecipientIfNeeded(ctx, letterID, nextApproval.ApproverID, existingUserIDs); err != nil {
return err
}
}
}
return nil
}
// getExistingRecipientUserIDs gets a set of existing recipient user IDs
func (p *LetterOutgoingProcessorImpl) getExistingRecipientUserIDs(ctx context.Context, letterID uuid.UUID) (map[uuid.UUID]bool, error) {
currentRecipients, err := p.recipientRepo.ListByLetter(ctx, letterID)
@ -665,12 +725,39 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett
return err
}
// Mark all other pending approvals in the same revision as rejected
allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID)
if err != nil {
return err
}
for i := range allApprovals {
// Only update other pending approvals from the same revision
if allApprovals[i].RevisionNumber == approval.RevisionNumber &&
allApprovals[i].ID != approval.ID &&
allApprovals[i].Status == entities.ApprovalStatusPending {
allApprovals[i].Status = entities.ApprovalStatusRejected
if err := p.approvalRepo.Update(txCtx, &allApprovals[i]); err != nil {
return err
}
}
}
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusRejected); err != nil {
return err
}
fromStatus := string(entities.LetterOutgoingStatusPendingApproval)
toStatus := string(entities.LetterOutgoingStatusRejected)
// Include rejection remarks in activity log context
var context entities.JSONB
if approval.Remarks != nil && *approval.Remarks != "" {
context = entities.JSONB{"remarks": *approval.Remarks, "revision_number": approval.RevisionNumber}
} else {
context = entities.JSONB{"revision_number": approval.RevisionNumber}
}
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionRejected,
@ -678,6 +765,79 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett
TargetID: &approval.ID,
FromStatus: &fromStatus,
ToStatus: &toStatus,
Context: context,
}
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
return err
}
return nil
})
}
func (p *LetterOutgoingProcessorImpl) ProcessRevision(ctx context.Context, letterID uuid.UUID, attachment entities.LetterOutgoingAttachment, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Get the current letter
letter, err := p.letterRepo.Get(txCtx, letterID)
if err != nil {
return err
}
// Increment revision number
letter.RevisionNumber++
// Set revision number on the new attachment
attachment.RevisionNumber = letter.RevisionNumber
// Add the new attachment
if err := p.attachmentRepo.Create(txCtx, &attachment); err != nil {
return err
}
// Update letter with new revision number
if err := p.letterRepo.Update(txCtx, letter); err != nil {
return err
}
// Update status to pending approval (ready for re-submission)
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil {
return err
}
// Get existing approvals for the current revision
approvals, err := p.approvalRepo.ListByLetter(txCtx, letterID)
if err != nil {
return err
}
// Create new approval records for the new revision
for _, approval := range approvals {
// Create a new approval for the new revision
newApproval := entities.LetterOutgoingApproval{
LetterID: approval.LetterID,
StepID: approval.StepID,
RevisionNumber: letter.RevisionNumber,
StepOrder: approval.StepOrder,
ParallelGroup: approval.ParallelGroup,
IsRequired: approval.IsRequired,
Status: entities.ApprovalStatusPending,
}
if err := p.approvalRepo.Create(txCtx, &newApproval); err != nil {
return err
}
}
// Log activity
fromStatus := string(entities.LetterOutgoingStatusRejected)
toStatus := string(entities.LetterOutgoingStatusPendingApproval)
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionRevised,
ActorUserID: &userID,
TargetID: &attachment.ID,
FromStatus: &fromStatus,
ToStatus: &toStatus,
Context: entities.JSONB{"attachment": attachment.FileName, "revision_number": letter.RevisionNumber},
}
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
return err
@ -732,6 +892,17 @@ func (p *LetterOutgoingProcessorImpl) RemoveRecipient(ctx context.Context, lette
func (p *LetterOutgoingProcessorImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Get the letter to get the current revision number
letter, err := p.letterRepo.Get(txCtx, letterID)
if err != nil {
return err
}
// Set revision number on all attachments
for i := range attachments {
attachments[i].RevisionNumber = letter.RevisionNumber
}
if err := p.attachmentRepo.CreateBulk(txCtx, attachments); err != nil {
return err
}
@ -808,7 +979,31 @@ func (p *LetterOutgoingProcessorImpl) DeleteDiscussion(ctx context.Context, id u
}
func (p *LetterOutgoingProcessorImpl) GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) {
return p.approvalRepo.ListByLetter(ctx, letterID)
// Get the letter first to know the current revision
letter, err := p.letterRepo.Get(ctx, letterID)
if err != nil {
return nil, err
}
return p.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
}
func (p *LetterOutgoingProcessorImpl) GetApprovalsByLetterAndRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) ([]entities.LetterOutgoingApproval, error) {
// Get all approvals for this letter
approvals, err := p.approvalRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Filter to only return approvals for the specified revision
var currentRevisionApprovals []entities.LetterOutgoingApproval
for _, approval := range approvals {
if approval.RevisionNumber == revisionNumber {
currentRevisionApprovals = append(currentRevisionApprovals, approval)
}
}
return currentRevisionApprovals, nil
}
func (p *LetterOutgoingProcessorImpl) GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error) {

View File

@ -106,6 +106,7 @@ type LetterOutgoingHandler interface {
SubmitForApproval(c *gin.Context)
ApproveOutgoingLetter(c *gin.Context)
RejectOutgoingLetter(c *gin.Context)
ReviseOutgoingLetter(c *gin.Context)
SendOutgoingLetter(c *gin.Context)
ArchiveOutgoingLetter(c *gin.Context)
GetLetterApprovalInfo(c *gin.Context)

View File

@ -193,6 +193,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
lettersch.POST("/outgoing/:id/submit", r.letterOutgoingHandler.SubmitForApproval)
lettersch.POST("/outgoing/:id/approve", r.letterOutgoingHandler.ApproveOutgoingLetter)
lettersch.POST("/outgoing/:id/reject", r.letterOutgoingHandler.RejectOutgoingLetter)
lettersch.POST("/outgoing/:id/revise", r.letterOutgoingHandler.ReviseOutgoingLetter)
lettersch.POST("/outgoing/:id/send", r.letterOutgoingHandler.SendOutgoingLetter)
lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter)
lettersch.POST("/outgoing/archive", r.letterOutgoingHandler.BulkArchiveOutgoingLetters)

View File

@ -628,7 +628,8 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l
return gorm.ErrInvalidData
}
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
// Get approvals for the current revision only
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil {
return err
}
@ -651,15 +652,7 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l
currentApproval.Remarks = req.Remarks
allApproved := true
for _, approval := range approvals {
if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending {
allApproved = false
break
}
}
err = s.processor.ProcessApproval(ctx, letterID, currentApproval, userID, allApproved)
err = s.processor.ProcessApproval(ctx, letterID, currentApproval, userID)
if err != nil {
return err
}
@ -691,7 +684,8 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le
return gorm.ErrInvalidData
}
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
// Get approvals for the current revision only
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil {
return err
}
@ -728,6 +722,35 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le
return nil
}
func (s *LetterOutgoingServiceImpl) ReviseOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ReviseLetterRequest) error {
userID := getUserIDFromContext(ctx)
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
if err != nil {
return err
}
// Can only revise rejected letters
if letter.Status != entities.LetterOutgoingStatusRejected {
return gorm.ErrInvalidData
}
attachment := entities.LetterOutgoingAttachment{
LetterID: letterID,
FileURL: req.FileURL,
FileName: req.FileName,
FileType: req.FileType,
UploadedBy: &userID,
}
err = s.processor.ProcessRevision(ctx, letterID, attachment, userID)
if err != nil {
return err
}
return nil
}
func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
userID := getUserIDFromContext(ctx)
@ -968,8 +991,8 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, l
return nil, err
}
// Get all approvals for this letter
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
// Get all approvals for this letter's current revision
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil {
return nil, err
}
@ -1030,6 +1053,15 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, l
}
}
// Add REVISE action if letter is rejected and user is the creator
if letter.Status == entities.LetterOutgoingStatusRejected && letter.CreatedBy == userID {
actions = append(actions, contract.ApprovalAction{
Type: "REVISE",
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/revise", letterID),
Method: "POST",
})
}
// Determine overall decision status
decisionStatus := "PENDING"
@ -1080,8 +1112,8 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovals(ctx context.Context, lett
return nil, err
}
// Get all approvals for this letter
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
// Get all approvals for this letter's current revision
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil {
return nil, err
}
@ -1435,6 +1467,7 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
IssueDate: letter.IssueDate,
Status: string(letter.Status),
ApprovalFlowID: letter.ApprovalFlowID,
RevisionNumber: letter.RevisionNumber,
CreatedBy: letter.CreatedBy,
CreatedAt: letter.CreatedAt,
UpdatedAt: letter.UpdatedAt,
@ -1778,7 +1811,15 @@ func (s *LetterOutgoingServiceImpl) BulkArchiveOutgoingLetters(ctx context.Conte
func (s *LetterOutgoingServiceImpl) sendStepApprovalNotifications(ctx context.Context, letterID uuid.UUID, subject string, stepOrder int) {
log.Printf("[DEBUG] sendStepApprovalNotifications START - LetterID: %s, StepOrder: %d", letterID.String(), stepOrder)
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
// Get the letter to know the current revision
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
if err != nil {
log.Printf("[ERROR] Failed to get letter: %v", err)
return
}
// Get approvals for the current revision only
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil {
log.Printf("[ERROR] Failed to get approvals: %v", err)
return

View File

@ -0,0 +1,8 @@
BEGIN;
DROP INDEX IF EXISTS idx_letters_outgoing_revision_number;
ALTER TABLE letters_outgoing
DROP COLUMN IF EXISTS revision_number;
COMMIT;

View File

@ -0,0 +1,8 @@
BEGIN;
ALTER TABLE letters_outgoing
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_revision_number ON letters_outgoing(revision_number);
COMMIT;

View File

@ -0,0 +1,14 @@
BEGIN;
-- Drop indexes
DROP INDEX IF EXISTS idx_letter_outgoing_attachments_revision;
DROP INDEX IF EXISTS idx_letter_outgoing_approvals_revision;
-- Remove revision_number from tables
ALTER TABLE letter_outgoing_attachments
DROP COLUMN IF EXISTS revision_number;
ALTER TABLE letter_outgoing_approvals
DROP COLUMN IF EXISTS revision_number;
COMMIT;

View File

@ -0,0 +1,15 @@
BEGIN;
-- Add revision_number to letter_outgoing_attachments
ALTER TABLE letter_outgoing_attachments
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
-- Add revision_number to letter_outgoing_approvals
ALTER TABLE letter_outgoing_approvals
ADD COLUMN IF NOT EXISTS revision_number INTEGER NOT NULL DEFAULT 0;
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_attachments_revision ON letter_outgoing_attachments(letter_id, revision_number);
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_revision ON letter_outgoing_approvals(letter_id, revision_number);
COMMIT;