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"` IssueDate time.Time `json:"issue_date"`
Status string `json:"status"` Status string `json:"status"`
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"` ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
RevisionNumber int `json:"revision_number"`
CreatedBy uuid.UUID `json:"created_by"` CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -157,6 +158,12 @@ type RejectLetterRequest struct {
Reason string `json:"reason" validate:"required"` 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 { type AddRecipientsRequest struct {
Recipients []CreateOutgoingLetterRecipient `json:"recipients" validate:"required,dive"` Recipients []CreateOutgoingLetterRecipient `json:"recipients" validate:"required,dive"`
} }

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ type LetterOutgoingService interface {
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) 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 SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
ArchiveOutgoingLetter(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"})) 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) { func (h *LetterOutgoingHandler) SendOutgoingLetter(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

@ -24,8 +24,9 @@ type LetterOutgoingProcessor interface {
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error 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 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 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 AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error
UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error
@ -40,6 +41,7 @@ type LetterOutgoingProcessor interface {
DeleteDiscussion(ctx context.Context, id uuid.UUID) error DeleteDiscussion(ctx context.Context, id uuid.UUID) error
GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, 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) GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error)
// GetOutgoingLetterWithDetails fetches letter with all related data // 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 { 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) // Find the minimum step order (first step)
minStepOrder := flow.Steps[0].StepOrder minStepOrder := flow.Steps[0].StepOrder
for _, step := range flow.Steps { for _, step := range flow.Steps {
@ -408,13 +416,14 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont
approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps)) approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps))
for i, step := range flow.Steps { for i, step := range flow.Steps {
approvals[i] = entities.LetterOutgoingApproval{ approvals[i] = entities.LetterOutgoingApproval{
LetterID: letterID, LetterID: letterID,
StepID: step.ID, StepID: step.ID,
StepOrder: step.StepOrder, RevisionNumber: letter.RevisionNumber,
ParallelGroup: step.ParallelGroup, StepOrder: step.StepOrder,
IsRequired: step.Required, ParallelGroup: step.ParallelGroup,
ApproverID: step.ApproverUserID, IsRequired: step.Required,
Status: entities.ApprovalStatusPending, ApproverID: step.ApproverUserID,
Status: entities.ApprovalStatusPending,
} }
// Set status based on step order // 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 { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Step 1: Update the approval record // Step 1: Update the approval record
if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil { if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil {
return err return err
} }
// Step 2: Get all approvals and organize by step // Step 2: Get all approvals FOR THE SAME REVISION and organize by step
approvalsByStep, err := p.getApprovalsByStep(txCtx, letterID) approvalsByStep, err := p.getApprovalsByStepForRevision(txCtx, letterID, approval.RevisionNumber)
if err != nil { if err != nil {
return err return err
} }
@ -499,12 +508,12 @@ func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, lette
// Step 3: Check if current step is completed // Step 3: Check if current step is completed
if p.isStepCompleted(approvalsByStep[approval.StepOrder]) { if p.isStepCompleted(approvalsByStep[approval.StepOrder]) {
// Step 4: Activate next step if exists // 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 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) { if p.areAllRequiredApprovalsCompleted(approvalsByStep) {
// Step 6: Update letter status to approved // Step 6: Update letter status to approved
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { 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 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 // isStepCompleted checks if all required approvals in a step are approved
func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool { func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool {
for _, approval := range stepApprovals { for _, approval := range stepApprovals {
@ -582,6 +609,39 @@ func (p *LetterOutgoingProcessorImpl) activateNextStep(ctx context.Context, lett
return nil 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 // getExistingRecipientUserIDs gets a set of existing recipient user IDs
func (p *LetterOutgoingProcessorImpl) getExistingRecipientUserIDs(ctx context.Context, letterID uuid.UUID) (map[uuid.UUID]bool, error) { func (p *LetterOutgoingProcessorImpl) getExistingRecipientUserIDs(ctx context.Context, letterID uuid.UUID) (map[uuid.UUID]bool, error) {
currentRecipients, err := p.recipientRepo.ListByLetter(ctx, letterID) currentRecipients, err := p.recipientRepo.ListByLetter(ctx, letterID)
@ -665,12 +725,39 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett
return err 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 { if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusRejected); err != nil {
return err return err
} }
fromStatus := string(entities.LetterOutgoingStatusPendingApproval) fromStatus := string(entities.LetterOutgoingStatusPendingApproval)
toStatus := string(entities.LetterOutgoingStatusRejected) 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{ activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID, LetterID: letterID,
ActionType: entities.LetterOutgoingActionRejected, ActionType: entities.LetterOutgoingActionRejected,
@ -678,6 +765,79 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett
TargetID: &approval.ID, TargetID: &approval.ID,
FromStatus: &fromStatus, FromStatus: &fromStatus,
ToStatus: &toStatus, 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 { if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
return err 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 { 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 { 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 { if err := p.attachmentRepo.CreateBulk(txCtx, attachments); err != nil {
return err 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) { 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) { 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) SubmitForApproval(c *gin.Context)
ApproveOutgoingLetter(c *gin.Context) ApproveOutgoingLetter(c *gin.Context)
RejectOutgoingLetter(c *gin.Context) RejectOutgoingLetter(c *gin.Context)
ReviseOutgoingLetter(c *gin.Context)
SendOutgoingLetter(c *gin.Context) SendOutgoingLetter(c *gin.Context)
ArchiveOutgoingLetter(c *gin.Context) ArchiveOutgoingLetter(c *gin.Context)
GetLetterApprovalInfo(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/submit", r.letterOutgoingHandler.SubmitForApproval)
lettersch.POST("/outgoing/:id/approve", r.letterOutgoingHandler.ApproveOutgoingLetter) lettersch.POST("/outgoing/:id/approve", r.letterOutgoingHandler.ApproveOutgoingLetter)
lettersch.POST("/outgoing/:id/reject", r.letterOutgoingHandler.RejectOutgoingLetter) 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/send", r.letterOutgoingHandler.SendOutgoingLetter)
lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter) lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter)
lettersch.POST("/outgoing/archive", r.letterOutgoingHandler.BulkArchiveOutgoingLetters) lettersch.POST("/outgoing/archive", r.letterOutgoingHandler.BulkArchiveOutgoingLetters)

View File

@ -628,7 +628,8 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l
return gorm.ErrInvalidData 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 { if err != nil {
return err return err
} }
@ -651,15 +652,7 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l
currentApproval.Remarks = req.Remarks currentApproval.Remarks = req.Remarks
allApproved := true err = s.processor.ProcessApproval(ctx, letterID, currentApproval, userID)
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)
if err != nil { if err != nil {
return err return err
} }
@ -691,7 +684,8 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le
return gorm.ErrInvalidData 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 { if err != nil {
return err return err
} }
@ -728,6 +722,35 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le
return nil 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 { func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
userID := getUserIDFromContext(ctx) userID := getUserIDFromContext(ctx)
@ -968,8 +991,8 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, l
return nil, err return nil, err
} }
// Get all approvals for this letter // Get all approvals for this letter's current revision
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil { if err != nil {
return nil, err 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 // Determine overall decision status
decisionStatus := "PENDING" decisionStatus := "PENDING"
@ -1080,8 +1112,8 @@ func (s *LetterOutgoingServiceImpl) GetLetterApprovals(ctx context.Context, lett
return nil, err return nil, err
} }
// Get all approvals for this letter // Get all approvals for this letter's current revision
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1435,6 +1467,7 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
IssueDate: letter.IssueDate, IssueDate: letter.IssueDate,
Status: string(letter.Status), Status: string(letter.Status),
ApprovalFlowID: letter.ApprovalFlowID, ApprovalFlowID: letter.ApprovalFlowID,
RevisionNumber: letter.RevisionNumber,
CreatedBy: letter.CreatedBy, CreatedBy: letter.CreatedBy,
CreatedAt: letter.CreatedAt, CreatedAt: letter.CreatedAt,
UpdatedAt: letter.UpdatedAt, 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) { 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) 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 { if err != nil {
log.Printf("[ERROR] Failed to get approvals: %v", err) log.Printf("[ERROR] Failed to get approvals: %v", err)
return 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;