Merge pull request 'add archive' (#1) from archived into main

Reviewed-on: ESLOGAD/eslogad-backend#1
This commit is contained in:
altru 2025-10-13 00:47:16 +00:00
commit e67b9a357f
15 changed files with 296 additions and 107 deletions

View File

@ -35,6 +35,8 @@ type LetterIncoming struct {
DueDate *time.Time `json:"due_date,omitempty"`
Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"`
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"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`

View File

@ -13,7 +13,7 @@ const (
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
LetterOutgoingStatusArchived LetterOutgoingStatus = "archived"
LetterOutgoingStatusRejected LetterOutgoingStatus = "rejected"
)
type LetterOutgoing struct {
@ -29,6 +29,8 @@ type LetterOutgoing struct {
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
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"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`

View File

@ -24,6 +24,7 @@ type LetterService interface {
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) 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)
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)
}
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"})
}

View File

@ -21,6 +21,7 @@ type LetterOutgoingProcessor interface {
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
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error
ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error
ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error
@ -52,7 +53,6 @@ type LetterOutgoingProcessor interface {
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)
GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error)
}
type LetterOutgoingProcessorImpl struct {
@ -356,8 +356,6 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le
activityLog.ActionType = entities.LetterOutgoingActionApproved
case entities.LetterOutgoingStatusSent:
activityLog.ActionType = entities.LetterOutgoingActionSent
case entities.LetterOutgoingStatusArchived:
activityLog.ActionType = entities.LetterOutgoingActionArchived
default:
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 {
flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID)
if err != nil {
@ -465,104 +485,173 @@ 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 {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
now := time.Now()
approval.Status = entities.ApprovalStatusApproved
approval.ApproverID = &userID
approval.ActedAt = &now
if err := p.approvalRepo.Update(txCtx, approval); err != nil {
// Step 1: Update the approval record
if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil {
return err
}
allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID)
// Step 2: Get all approvals and organize by step
approvalsByStep, err := p.getApprovalsByStep(txCtx, letterID)
if err != nil {
return err
}
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
for _, a := range allApprovals {
approvalsByStep[a.StepOrder] = append(approvalsByStep[a.StepOrder], a)
}
currentStepCompleted := true
for _, a := range approvalsByStep[approval.StepOrder] {
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
currentStepCompleted = false
break
// 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
}
}
// If current step is completed, activate the next step and add approvers as recipients
if currentStepCompleted {
nextStepOrder := approval.StepOrder + 1
if nextStepApprovals, exists := approvalsByStep[nextStepOrder]; exists {
currentRecipients, err := p.recipientRepo.ListByLetter(txCtx, letterID)
if err != nil {
return err
}
existingUserIDs := make(map[uuid.UUID]bool)
for _, recipient := range currentRecipients {
if recipient.UserID != nil {
existingUserIDs[*recipient.UserID] = true
}
}
for _, nextApproval := range nextStepApprovals {
if nextApproval.Status == entities.ApprovalStatusNotStarted {
nextApproval.Status = entities.ApprovalStatusPending
if err := p.approvalRepo.Update(txCtx, &nextApproval); err != nil {
return err
}
}
if nextApproval.ApproverID != nil && !existingUserIDs[*nextApproval.ApproverID] {
newRecipient := entities.LetterOutgoingRecipient{
LetterID: letterID,
UserID: nextApproval.ApproverID,
IsPrimary: false,
Status: "unread",
IsArchived: false,
}
if err := p.recipientRepo.Create(txCtx, &newRecipient); err != nil {
return err
}
existingUserIDs[*nextApproval.ApproverID] = true
}
}
}
}
// Check if all required approvals are completed
allRequiredApproved := true
for _, a := range allApprovals {
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
allRequiredApproved = false
break
}
}
// Update letter status if all required approvals are done
if allRequiredApproved {
// 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
}
}
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionApproved,
ActorUserID: &userID,
TargetID: &approval.ID,
// 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()
approval.Status = entities.ApprovalStatusApproved
approval.ApproverID = &userID
approval.ActedAt = &now
return p.approvalRepo.Update(ctx, approval)
}
// 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 {
return nil, err
}
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
for _, approval := range allApprovals {
approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval)
}
return approvalsByStep, nil
}
// isStepCompleted checks if all required approvals in a step are approved
func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool {
for _, approval := range stepApprovals {
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
return false
}
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil {
}
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
}
// Get existing recipients to avoid duplicates
existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID)
if err != nil {
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)
for _, recipient := range currentRecipients {
if recipient.UserID != nil {
existingUserIDs[*recipient.UserID] = true
}
}
return existingUserIDs, nil
}
// activateApprovalIfNotStarted changes approval status from not_started to pending
func (p *LetterOutgoingProcessorImpl) activateApprovalIfNotStarted(ctx context.Context, approval *entities.LetterOutgoingApproval) error {
if approval.Status != entities.ApprovalStatusNotStarted {
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
}
newRecipient := entities.LetterOutgoingRecipient{
LetterID: letterID,
UserID: approverID,
IsPrimary: false,
Status: "unread",
IsArchived: false,
}
if err := p.recipientRepo.Create(ctx, &newRecipient); err != nil {
return err
}
existingUserIDs[*approverID] = true
return nil
}
// areAllRequiredApprovalsCompleted checks if all required approvals are completed
func (p *LetterOutgoingProcessorImpl) areAllRequiredApprovalsCompleted(approvalsByStep map[int][]entities.LetterOutgoingApproval) bool {
for _, stepApprovals := range approvalsByStep {
for _, approval := range stepApprovals {
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
return false
}
}
}
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{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionApproved,
ActorUserID: &userID,
TargetID: &approvalID,
}
return p.activityLogRepo.Create(ctx, activityLog)
}
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
}
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil {
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusRejected); err != nil {
return err
}
fromStatus := string(entities.LetterOutgoingStatusPendingApproval)
toStatus := string(entities.LetterOutgoingStatusDraft)
toStatus := string(entities.LetterOutgoingStatusRejected)
activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID,
ActionType: entities.LetterOutgoingActionRejected,

View File

@ -623,6 +623,10 @@ func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, le
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
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)

View File

@ -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) {
db := DBFromContext(ctx, r.db)
now := time.Now()
result := db.WithContext(ctx).
Model(&entities.LetterOutgoing{}).
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
}
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) {
db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id)
@ -104,11 +120,7 @@ func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoing
// Apply is_archived filter
if filter.IsArchived != nil {
if *filter.IsArchived {
query = query.Where("letters_outgoing.status = 'archived'")
} else {
query = query.Where("letters_outgoing.status != 'archived'")
}
query = query.Where("letters_outgoing.is_archived = ?", *filter.IsArchived)
}
if filter.Status != nil {

View File

@ -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) {
db := DBFromContext(ctx, r.db)
// For incoming letters, we archive the recipients, not the letter itself
// The letter status remains as is (new, in_progress, or completed)
now := time.Now()
result := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}).
Where("letter_id IN ?", letterIDs).
Update("is_archived", true)
Model(&entities.LetterIncoming{}).
Where("id IN ? AND deleted_at IS NULL", letterIDs).
Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
})
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
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
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 {
query = query.Where("letter_incoming_recipients.is_archived = ?", true)
query = query.Where("letters_incoming.is_archived = ?", true)
} 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)
}
}

View File

@ -83,6 +83,7 @@ type LetterHandler interface {
UpdateIncomingLetter(c *gin.Context)
DeleteIncomingLetter(c *gin.Context)
BulkArchiveIncomingLetters(c *gin.Context)
ArchiveIncomingLetter(c *gin.Context)
CreateDispositions(c *gin.Context)
GetEnhancedDispositionsByLetter(c *gin.Context)

View File

@ -179,6 +179,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead)
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
lettersch.PUT("/incoming/:id/archive", r.letterHandler.ArchiveIncomingLetter)
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters)

View File

@ -753,13 +753,13 @@ func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, l
return err
}
// Can only archive sent letters
if letter.Status != entities.LetterOutgoingStatusSent {
return gorm.ErrInvalidData
}
fromStatus := string(entities.LetterOutgoingStatusSent)
toStatus := string(entities.LetterOutgoingStatusArchived)
return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus)
// Use the new archive method instead of changing status
return s.processor.ArchiveOutgoingLetter(ctx, letterID, userID)
}
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {

View File

@ -32,6 +32,7 @@ type LetterProcessor interface {
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) 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)
// 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) {
// Extract user context to archive only for the current user
appCtx := appcontext.FromGinContext(ctx)
userID := appCtx.UserID
// Archive letters only for the current user
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
// Archive the letters themselves
archivedCount, err := s.processor.BulkArchiveIncomingLetters(ctx, letterIDs)
if err != nil {
return nil, err
}
@ -833,6 +830,10 @@ func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, lett
}, 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) {
// Extract user_ids from mentions
userIDs := s.extractUserIDsFromMentions(mentions)

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;