add archive
This commit is contained in:
parent
b81c0be3c4
commit
e3cd04a90e
@ -35,6 +35,8 @@ type LetterIncoming struct {
|
|||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"`
|
Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"`
|
||||||
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
|
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
|
||||||
|
IsArchived bool `gorm:"not null;default:false" json:"is_archived"`
|
||||||
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const (
|
|||||||
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
|
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
|
||||||
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
|
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
|
||||||
LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
|
LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
|
||||||
LetterOutgoingStatusArchived LetterOutgoingStatus = "archived"
|
LetterOutgoingStatusRejected LetterOutgoingStatus = "rejected"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LetterOutgoing struct {
|
type LetterOutgoing struct {
|
||||||
@ -29,6 +29,8 @@ type LetterOutgoing struct {
|
|||||||
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"`
|
||||||
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"`
|
||||||
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
|
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type LetterService interface {
|
|||||||
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error)
|
||||||
|
ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
@ -465,3 +466,19 @@ func (h *LetterHandler) BulkArchiveIncomingLetters(c *gin.Context) {
|
|||||||
|
|
||||||
h.respondSuccess(c, http.StatusOK, resp)
|
h.respondSuccess(c, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) ArchiveIncomingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
h.respondError(c, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.svc.ArchiveIncomingLetter(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
h.handleServiceError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{Message: "archived"})
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ type LetterOutgoingProcessor interface {
|
|||||||
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error
|
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error
|
||||||
|
|
||||||
UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) error
|
UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) 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, allApproved bool) error
|
||||||
@ -52,7 +53,6 @@ type LetterOutgoingProcessor interface {
|
|||||||
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error)
|
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error)
|
||||||
GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error)
|
GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error)
|
||||||
GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error)
|
GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterOutgoingProcessorImpl struct {
|
type LetterOutgoingProcessorImpl struct {
|
||||||
@ -356,8 +356,6 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le
|
|||||||
activityLog.ActionType = entities.LetterOutgoingActionApproved
|
activityLog.ActionType = entities.LetterOutgoingActionApproved
|
||||||
case entities.LetterOutgoingStatusSent:
|
case entities.LetterOutgoingStatusSent:
|
||||||
activityLog.ActionType = entities.LetterOutgoingActionSent
|
activityLog.ActionType = entities.LetterOutgoingActionSent
|
||||||
case entities.LetterOutgoingStatusArchived:
|
|
||||||
activityLog.ActionType = entities.LetterOutgoingActionArchived
|
|
||||||
default:
|
default:
|
||||||
activityLog.ActionType = entities.LetterOutgoingActionUpdated
|
activityLog.ActionType = entities.LetterOutgoingActionUpdated
|
||||||
}
|
}
|
||||||
@ -370,6 +368,28 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *LetterOutgoingProcessorImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error {
|
||||||
|
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
// Archive the letter using the new flag
|
||||||
|
if err := p.letterRepo.Archive(txCtx, letterID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the activity
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
ActionType: entities.LetterOutgoingActionArchived,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error {
|
func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error {
|
||||||
flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID)
|
flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -465,42 +485,110 @@ 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, allApproved bool) 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
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Check if all required approvals are completed
|
||||||
|
if p.areAllRequiredApprovalsCompleted(approvalsByStep) {
|
||||||
|
// Step 6: Update letter status to approved
|
||||||
|
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Log the activity
|
||||||
|
return p.logApprovalActivity(txCtx, letterID, approval.ID, userID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateApprovalRecord updates the approval with approver info and timestamp
|
||||||
|
func (p *LetterOutgoingProcessorImpl) updateApprovalRecord(ctx context.Context, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
approval.Status = entities.ApprovalStatusApproved
|
approval.Status = entities.ApprovalStatusApproved
|
||||||
approval.ApproverID = &userID
|
approval.ApproverID = &userID
|
||||||
approval.ActedAt = &now
|
approval.ActedAt = &now
|
||||||
|
|
||||||
if err := p.approvalRepo.Update(txCtx, approval); err != nil {
|
return p.approvalRepo.Update(ctx, approval)
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID)
|
// getApprovalsByStep fetches all approvals and organizes them by step order
|
||||||
|
func (p *LetterOutgoingProcessorImpl) getApprovalsByStep(ctx context.Context, letterID uuid.UUID) (map[int][]entities.LetterOutgoingApproval, error) {
|
||||||
|
allApprovals, err := p.approvalRepo.ListByLetter(ctx, letterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
|
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
|
||||||
for _, a := range allApprovals {
|
for _, approval := range allApprovals {
|
||||||
approvalsByStep[a.StepOrder] = append(approvalsByStep[a.StepOrder], a)
|
approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentStepCompleted := true
|
return approvalsByStep, nil
|
||||||
for _, a := range approvalsByStep[approval.StepOrder] {
|
}
|
||||||
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
|
|
||||||
currentStepCompleted = false
|
// isStepCompleted checks if all required approvals in a step are approved
|
||||||
break
|
func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool {
|
||||||
|
for _, approval := range stepApprovals {
|
||||||
|
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// activateNextStep activates the next approval step and adds approvers as recipients
|
||||||
|
func (p *LetterOutgoingProcessorImpl) activateNextStep(ctx context.Context, letterID uuid.UUID, currentStepOrder int, approvalsByStep map[int][]entities.LetterOutgoingApproval) error {
|
||||||
|
nextStepOrder := currentStepOrder + 1
|
||||||
|
nextStepApprovals, exists := approvalsByStep[nextStepOrder]
|
||||||
|
if !exists {
|
||||||
|
return nil // No next step
|
||||||
|
}
|
||||||
|
|
||||||
// If current step is completed, activate the next step and add approvers as recipients
|
// Get existing recipients to avoid duplicates
|
||||||
if currentStepCompleted {
|
existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID)
|
||||||
nextStepOrder := approval.StepOrder + 1
|
|
||||||
if nextStepApprovals, exists := approvalsByStep[nextStepOrder]; exists {
|
|
||||||
currentRecipients, err := p.recipientRepo.ListByLetter(txCtx, letterID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process each approval in the next step
|
||||||
|
for _, nextApproval := range nextStepApprovals {
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
existingUserIDs := make(map[uuid.UUID]bool)
|
existingUserIDs := make(map[uuid.UUID]bool)
|
||||||
for _, recipient := range currentRecipients {
|
for _, recipient := range currentRecipients {
|
||||||
if recipient.UserID != nil {
|
if recipient.UserID != nil {
|
||||||
@ -508,61 +596,62 @@ func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, lette
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, nextApproval := range nextStepApprovals {
|
return existingUserIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
if nextApproval.Status == entities.ApprovalStatusNotStarted {
|
// activateApprovalIfNotStarted changes approval status from not_started to pending
|
||||||
nextApproval.Status = entities.ApprovalStatusPending
|
func (p *LetterOutgoingProcessorImpl) activateApprovalIfNotStarted(ctx context.Context, approval *entities.LetterOutgoingApproval) error {
|
||||||
if err := p.approvalRepo.Update(txCtx, &nextApproval); err != nil {
|
if approval.Status != entities.ApprovalStatusNotStarted {
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
approval.Status = entities.ApprovalStatusPending
|
||||||
|
return p.approvalRepo.Update(ctx, approval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addApproverAsRecipientIfNeeded adds an approver as a recipient if they don't exist
|
||||||
|
func (p *LetterOutgoingProcessorImpl) addApproverAsRecipientIfNeeded(ctx context.Context, letterID uuid.UUID, approverID *uuid.UUID, existingUserIDs map[uuid.UUID]bool) error {
|
||||||
|
if approverID == nil || existingUserIDs[*approverID] {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if nextApproval.ApproverID != nil && !existingUserIDs[*nextApproval.ApproverID] {
|
|
||||||
newRecipient := entities.LetterOutgoingRecipient{
|
newRecipient := entities.LetterOutgoingRecipient{
|
||||||
LetterID: letterID,
|
LetterID: letterID,
|
||||||
UserID: nextApproval.ApproverID,
|
UserID: approverID,
|
||||||
IsPrimary: false,
|
IsPrimary: false,
|
||||||
Status: "unread",
|
Status: "unread",
|
||||||
IsArchived: false,
|
IsArchived: false,
|
||||||
}
|
}
|
||||||
if err := p.recipientRepo.Create(txCtx, &newRecipient); err != nil {
|
|
||||||
|
if err := p.recipientRepo.Create(ctx, &newRecipient); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingUserIDs[*nextApproval.ApproverID] = true
|
existingUserIDs[*approverID] = true
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all required approvals are completed
|
// areAllRequiredApprovalsCompleted checks if all required approvals are completed
|
||||||
allRequiredApproved := true
|
func (p *LetterOutgoingProcessorImpl) areAllRequiredApprovalsCompleted(approvalsByStep map[int][]entities.LetterOutgoingApproval) bool {
|
||||||
for _, a := range allApprovals {
|
for _, stepApprovals := range approvalsByStep {
|
||||||
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
|
for _, approval := range stepApprovals {
|
||||||
allRequiredApproved = false
|
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
||||||
break
|
return false
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update letter status if all required approvals are done
|
|
||||||
if allRequiredApproved {
|
|
||||||
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// logApprovalActivity creates an activity log for the approval action
|
||||||
|
func (p *LetterOutgoingProcessorImpl) logApprovalActivity(ctx context.Context, letterID, approvalID uuid.UUID, userID uuid.UUID) error {
|
||||||
activityLog := &entities.LetterOutgoingActivityLog{
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
LetterID: letterID,
|
LetterID: letterID,
|
||||||
ActionType: entities.LetterOutgoingActionApproved,
|
ActionType: entities.LetterOutgoingActionApproved,
|
||||||
ActorUserID: &userID,
|
ActorUserID: &userID,
|
||||||
TargetID: &approval.ID,
|
TargetID: &approvalID,
|
||||||
}
|
}
|
||||||
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
return p.activityLogRepo.Create(ctx, activityLog)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error {
|
func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error {
|
||||||
@ -576,12 +665,12 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); 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.LetterOutgoingStatusDraft)
|
toStatus := string(entities.LetterOutgoingStatusRejected)
|
||||||
activityLog := &entities.LetterOutgoingActivityLog{
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
LetterID: letterID,
|
LetterID: letterID,
|
||||||
ActionType: entities.LetterOutgoingActionRejected,
|
ActionType: entities.LetterOutgoingActionRejected,
|
||||||
|
|||||||
@ -623,6 +623,10 @@ func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, le
|
|||||||
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
return p.letterRepo.Archive(ctx, letterID)
|
||||||
|
}
|
||||||
|
|
||||||
// BulkArchiveIncomingLettersForUser archives letters for a specific user only
|
// BulkArchiveIncomingLettersForUser archives letters for a specific user only
|
||||||
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
||||||
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)
|
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)
|
||||||
|
|||||||
@ -54,13 +54,29 @@ func (r *LetterOutgoingRepository) SoftDelete(ctx context.Context, id uuid.UUID)
|
|||||||
|
|
||||||
func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
result := db.WithContext(ctx).
|
result := db.WithContext(ctx).
|
||||||
Model(&entities.LetterOutgoing{}).
|
Model(&entities.LetterOutgoing{}).
|
||||||
Where("id IN ? AND deleted_at IS NULL", letterIDs).
|
Where("id IN ? AND deleted_at IS NULL", letterIDs).
|
||||||
Update("status", "archived")
|
Updates(map[string]interface{}{
|
||||||
|
"is_archived": true,
|
||||||
|
"archived_at": now,
|
||||||
|
})
|
||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) Archive(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
return db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterOutgoing{}).
|
||||||
|
Where("id = ? AND deleted_at IS NULL", letterID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"is_archived": true,
|
||||||
|
"archived_at": now,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
|
func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
|
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
|
||||||
@ -103,11 +119,7 @@ func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoing
|
|||||||
|
|
||||||
// Apply is_archived filter
|
// Apply is_archived filter
|
||||||
if filter.IsArchived != nil {
|
if filter.IsArchived != nil {
|
||||||
if *filter.IsArchived {
|
query = query.Where("letters_outgoing.is_archived = ?", *filter.IsArchived)
|
||||||
query = query.Where("letters_outgoing.status = 'archived'")
|
|
||||||
} else {
|
|
||||||
query = query.Where("letters_outgoing.status != 'archived'")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.Status != nil {
|
if filter.Status != nil {
|
||||||
|
|||||||
@ -45,15 +45,29 @@ func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID)
|
|||||||
|
|
||||||
func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
// For incoming letters, we archive the recipients, not the letter itself
|
now := time.Now()
|
||||||
// The letter status remains as is (new, in_progress, or completed)
|
|
||||||
result := db.WithContext(ctx).
|
result := db.WithContext(ctx).
|
||||||
Model(&entities.LetterIncomingRecipient{}).
|
Model(&entities.LetterIncoming{}).
|
||||||
Where("letter_id IN ?", letterIDs).
|
Where("id IN ? AND deleted_at IS NULL", letterIDs).
|
||||||
Update("is_archived", true)
|
Updates(map[string]interface{}{
|
||||||
|
"is_archived": true,
|
||||||
|
"archived_at": now,
|
||||||
|
})
|
||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) Archive(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
return db.WithContext(ctx).
|
||||||
|
Model(&entities.LetterIncoming{}).
|
||||||
|
Where("id = ? AND deleted_at IS NULL", letterID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"is_archived": true,
|
||||||
|
"archived_at": now,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
// BulkArchiveForUser archives letters for a specific user only
|
// BulkArchiveForUser archives letters for a specific user only
|
||||||
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
@ -122,9 +136,9 @@ func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncoming
|
|||||||
|
|
||||||
if filter.IsArchived != nil {
|
if filter.IsArchived != nil {
|
||||||
if *filter.IsArchived {
|
if *filter.IsArchived {
|
||||||
query = query.Where("letter_incoming_recipients.is_archived = ?", true)
|
query = query.Where("letters_incoming.is_archived = ?", true)
|
||||||
} else {
|
} else {
|
||||||
query = query.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false)
|
query = query.Where("letters_incoming.is_archived = ? OR letters_incoming.is_archived IS NULL", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,7 @@ type LetterHandler interface {
|
|||||||
UpdateIncomingLetter(c *gin.Context)
|
UpdateIncomingLetter(c *gin.Context)
|
||||||
DeleteIncomingLetter(c *gin.Context)
|
DeleteIncomingLetter(c *gin.Context)
|
||||||
BulkArchiveIncomingLetters(c *gin.Context)
|
BulkArchiveIncomingLetters(c *gin.Context)
|
||||||
|
ArchiveIncomingLetter(c *gin.Context)
|
||||||
|
|
||||||
CreateDispositions(c *gin.Context)
|
CreateDispositions(c *gin.Context)
|
||||||
GetEnhancedDispositionsByLetter(c *gin.Context)
|
GetEnhancedDispositionsByLetter(c *gin.Context)
|
||||||
|
|||||||
@ -179,6 +179,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead)
|
lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead)
|
||||||
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||||
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
|
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
|
||||||
|
lettersch.PUT("/incoming/:id/archive", r.letterHandler.ArchiveIncomingLetter)
|
||||||
|
|
||||||
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
||||||
lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters)
|
lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters)
|
||||||
|
|||||||
@ -750,13 +750,13 @@ func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, l
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Can only archive sent letters
|
||||||
if letter.Status != entities.LetterOutgoingStatusSent {
|
if letter.Status != entities.LetterOutgoingStatusSent {
|
||||||
return gorm.ErrInvalidData
|
return gorm.ErrInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
fromStatus := string(entities.LetterOutgoingStatusSent)
|
// Use the new archive method instead of changing status
|
||||||
toStatus := string(entities.LetterOutgoingStatusArchived)
|
return s.processor.ArchiveOutgoingLetter(ctx, letterID, userID)
|
||||||
return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {
|
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ type LetterProcessor interface {
|
|||||||
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
|
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
|
||||||
|
ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error)
|
BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error)
|
||||||
|
|
||||||
// Batch loading methods
|
// Batch loading methods
|
||||||
@ -816,12 +817,8 @@ func (s *LetterServiceImpl) GetLetterCTA(ctx context.Context, letterID uuid.UUID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
||||||
// Extract user context to archive only for the current user
|
// Archive the letters themselves
|
||||||
appCtx := appcontext.FromGinContext(ctx)
|
archivedCount, err := s.processor.BulkArchiveIncomingLetters(ctx, letterIDs)
|
||||||
userID := appCtx.UserID
|
|
||||||
|
|
||||||
// Archive letters only for the current user
|
|
||||||
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -833,6 +830,10 @@ func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, lett
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
return s.processor.ArchiveIncomingLetter(ctx, letterID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) {
|
func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) {
|
||||||
// Extract user_ids from mentions
|
// Extract user_ids from mentions
|
||||||
userIDs := s.extractUserIDsFromMentions(mentions)
|
userIDs := s.extractUserIDsFromMentions(mentions)
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_letters_outgoing_is_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_outgoing_archived_at;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_outgoing_archived_status;
|
||||||
|
|
||||||
|
ALTER TABLE letters_outgoing
|
||||||
|
DROP COLUMN IF EXISTS is_archived,
|
||||||
|
DROP COLUMN IF EXISTS archived_at;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE letters_outgoing
|
||||||
|
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_is_archived ON letters_outgoing(is_archived);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_at ON letters_outgoing(archived_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_status ON letters_outgoing(is_archived, status);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_letters_incoming_is_archived;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_incoming_archived_at;
|
||||||
|
DROP INDEX IF EXISTS idx_letters_incoming_archived_status;
|
||||||
|
|
||||||
|
ALTER TABLE letters_incoming
|
||||||
|
DROP COLUMN IF EXISTS is_archived,
|
||||||
|
DROP COLUMN IF EXISTS archived_at;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE letters_incoming
|
||||||
|
ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_incoming_is_archived ON letters_incoming(is_archived);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_at ON letters_incoming(archived_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_status ON letters_incoming(is_archived, status);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
x
Reference in New Issue
Block a user