add revision number
This commit is contained in:
parent
4e58ce95fb
commit
e58472c963
@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ 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"`
|
||||||
|
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
|
||||||
StepOrder int `gorm:"not null" json:"step_order"`
|
StepOrder int `gorm:"not null" json:"step_order"`
|
||||||
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
|
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
|
||||||
IsRequired bool `gorm:"default:true" json:"is_required"`
|
IsRequired bool `gorm:"default:true" json:"is_required"`
|
||||||
|
|||||||
@ -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"`
|
||||||
@ -69,6 +70,7 @@ func (LetterOutgoingRecipient) TableName() string { return "letter_outgoing_reci
|
|||||||
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"`
|
||||||
|
RevisionNumber int `gorm:"not null;default:0" json:"revision_number"`
|
||||||
FileURL string `gorm:"not null" json:"file_url"`
|
FileURL string `gorm:"not null" json:"file_url"`
|
||||||
FileName string `gorm:"not null" json:"file_name"`
|
FileName string `gorm:"not null" json:"file_name"`
|
||||||
FileType string `gorm:"not null" json:"file_type"`
|
FileType string `gorm:"not null" json:"file_type"`
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -410,6 +418,7 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont
|
|||||||
approvals[i] = entities.LetterOutgoingApproval{
|
approvals[i] = entities.LetterOutgoingApproval{
|
||||||
LetterID: letterID,
|
LetterID: letterID,
|
||||||
StepID: step.ID,
|
StepID: step.ID,
|
||||||
|
RevisionNumber: letter.RevisionNumber,
|
||||||
StepOrder: step.StepOrder,
|
StepOrder: step.StepOrder,
|
||||||
ParallelGroup: step.ParallelGroup,
|
ParallelGroup: step.ParallelGroup,
|
||||||
IsRequired: step.Required,
|
IsRequired: step.Required,
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
Loading…
x
Reference in New Issue
Block a user