package repository import ( "errors" "fmt" "time" "accounting-app/internal/models" "gorm.io/gorm" ) // Repayment repository errors var ( ErrRepaymentPlanNotFound = errors.New("repayment plan not found") ErrInstallmentNotFound = errors.New("installment not found") ErrReminderNotFound = errors.New("reminder not found") ) // RepaymentRepository handles database operations for repayment plans and installments type RepaymentRepository struct { db *gorm.DB } // NewRepaymentRepository creates a new RepaymentRepository instance func NewRepaymentRepository(db *gorm.DB) *RepaymentRepository { return &RepaymentRepository{db: db} } // ======================================== // Repayment Plan Operations // ======================================== // CreatePlan creates a new repayment plan func (r *RepaymentRepository) CreatePlan(plan *models.RepaymentPlan) error { if err := r.db.Create(plan).Error; err != nil { return fmt.Errorf("failed to create repayment plan: %w", err) } return nil } // GetPlanByID retrieves a repayment plan by its ID func (r *RepaymentRepository) GetPlanByID(userID uint, id uint) (*models.RepaymentPlan, error) { var plan models.RepaymentPlan if err := r.db.Where("user_id = ?", userID).Preload("Bill").Preload("Bill.Account").Preload("Installments").First(&plan, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrRepaymentPlanNotFound } return nil, fmt.Errorf("failed to get repayment plan: %w", err) } return &plan, nil } // GetPlanByBillID retrieves a repayment plan by bill ID func (r *RepaymentRepository) GetPlanByBillID(userID uint, billID uint) (*models.RepaymentPlan, error) { var plan models.RepaymentPlan if err := r.db.Where("user_id = ? AND bill_id = ?", userID, billID). Preload("Bill"). Preload("Bill.Account"). Preload("Installments"). First(&plan).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrRepaymentPlanNotFound } return nil, fmt.Errorf("failed to get repayment plan by bill: %w", err) } return &plan, nil } // GetActivePlans retrieves all active repayment plans func (r *RepaymentRepository) GetActivePlans(userID uint) ([]models.RepaymentPlan, error) { var plans []models.RepaymentPlan if err := r.db.Where("user_id = ? AND status = ?", userID, models.RepaymentPlanStatusActive). Preload("Bill"). Preload("Bill.Account"). Preload("Installments"). Find(&plans).Error; err != nil { return nil, fmt.Errorf("failed to get active plans: %w", err) } return plans, nil } // UpdatePlan updates an existing repayment plan func (r *RepaymentRepository) UpdatePlan(plan *models.RepaymentPlan) error { var existing models.RepaymentPlan if err := r.db.First(&existing, plan.ID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrRepaymentPlanNotFound } return fmt.Errorf("failed to check plan existence: %w", err) } if err := r.db.Save(plan).Error; err != nil { return fmt.Errorf("failed to update repayment plan: %w", err) } return nil } // UpdatePlanStatus updates the status of a repayment plan func (r *RepaymentRepository) UpdatePlanStatus(id uint, status models.RepaymentPlanStatus) error { result := r.db.Model(&models.RepaymentPlan{}).Where("id = ?", id).Update("status", status) if result.Error != nil { return fmt.Errorf("failed to update plan status: %w", result.Error) } if result.RowsAffected == 0 { return ErrRepaymentPlanNotFound } return nil } // DeletePlan deletes a repayment plan and its installments func (r *RepaymentRepository) DeletePlan(id uint) error { return r.db.Transaction(func(tx *gorm.DB) error { // Delete installments first if err := tx.Where("plan_id = ?", id).Delete(&models.RepaymentInstallment{}).Error; err != nil { return fmt.Errorf("failed to delete installments: %w", err) } // Delete the plan result := tx.Delete(&models.RepaymentPlan{}, id) if result.Error != nil { return fmt.Errorf("failed to delete plan: %w", result.Error) } if result.RowsAffected == 0 { return ErrRepaymentPlanNotFound } return nil }) } // ======================================== // Installment Operations // ======================================== // CreateInstallment creates a new installment func (r *RepaymentRepository) CreateInstallment(installment *models.RepaymentInstallment) error { if err := r.db.Create(installment).Error; err != nil { return fmt.Errorf("failed to create installment: %w", err) } return nil } // GetInstallmentByID retrieves an installment by its ID func (r *RepaymentRepository) GetInstallmentByID(id uint) (*models.RepaymentInstallment, error) { var installment models.RepaymentInstallment if err := r.db.Preload("Plan").Preload("Plan.Bill").First(&installment, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrInstallmentNotFound } return nil, fmt.Errorf("failed to get installment: %w", err) } return &installment, nil } // GetInstallmentsByPlanID retrieves all installments for a plan func (r *RepaymentRepository) GetInstallmentsByPlanID(planID uint) ([]models.RepaymentInstallment, error) { var installments []models.RepaymentInstallment if err := r.db.Where("plan_id = ?", planID). Order("sequence ASC"). Find(&installments).Error; err != nil { return nil, fmt.Errorf("failed to get installments: %w", err) } return installments, nil } // GetPendingInstallments retrieves all pending installments func (r *RepaymentRepository) GetPendingInstallments() ([]models.RepaymentInstallment, error) { var installments []models.RepaymentInstallment if err := r.db.Where("status = ?", models.RepaymentInstallmentStatusPending). Preload("Plan"). Preload("Plan.Bill"). Preload("Plan.Bill.Account"). Order("due_date ASC"). Find(&installments).Error; err != nil { return nil, fmt.Errorf("failed to get pending installments: %w", err) } return installments, nil } // GetInstallmentsDueInRange retrieves installments due within a date range func (r *RepaymentRepository) GetInstallmentsDueInRange(startDate, endDate time.Time) ([]models.RepaymentInstallment, error) { var installments []models.RepaymentInstallment if err := r.db.Where("due_date >= ? AND due_date <= ? AND status = ?", startDate, endDate, models.RepaymentInstallmentStatusPending). Preload("Plan"). Preload("Plan.Bill"). Preload("Plan.Bill.Account"). Order("due_date ASC"). Find(&installments).Error; err != nil { return nil, fmt.Errorf("failed to get installments due in range: %w", err) } return installments, nil } // UpdateInstallment updates an existing installment func (r *RepaymentRepository) UpdateInstallment(installment *models.RepaymentInstallment) error { var existing models.RepaymentInstallment if err := r.db.First(&existing, installment.ID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrInstallmentNotFound } return fmt.Errorf("failed to check installment existence: %w", err) } if err := r.db.Save(installment).Error; err != nil { return fmt.Errorf("failed to update installment: %w", err) } return nil } // UpdateInstallmentStatus updates the status of an installment func (r *RepaymentRepository) UpdateInstallmentStatus(id uint, status models.RepaymentInstallmentStatus) error { result := r.db.Model(&models.RepaymentInstallment{}).Where("id = ?", id).Update("status", status) if result.Error != nil { return fmt.Errorf("failed to update installment status: %w", result.Error) } if result.RowsAffected == 0 { return ErrInstallmentNotFound } return nil } // MarkInstallmentAsPaid marks an installment as paid func (r *RepaymentRepository) MarkInstallmentAsPaid(id uint, paidAmount float64, paidAt time.Time) error { result := r.db.Model(&models.RepaymentInstallment{}).Where("id = ?", id).Updates(map[string]interface{}{ "status": models.RepaymentInstallmentStatusPaid, "paid_amount": paidAmount, "paid_at": paidAt, }) if result.Error != nil { return fmt.Errorf("failed to mark installment as paid: %w", result.Error) } if result.RowsAffected == 0 { return ErrInstallmentNotFound } return nil } // ======================================== // Payment Reminder Operations // ======================================== // CreateReminder creates a new payment reminder func (r *RepaymentRepository) CreateReminder(reminder *models.PaymentReminder) error { if err := r.db.Create(reminder).Error; err != nil { return fmt.Errorf("failed to create reminder: %w", err) } return nil } // GetReminderByID retrieves a reminder by its ID func (r *RepaymentRepository) GetReminderByID(id uint) (*models.PaymentReminder, error) { var reminder models.PaymentReminder if err := r.db.Preload("Bill").Preload("Bill.Account").Preload("Installment").First(&reminder, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrReminderNotFound } return nil, fmt.Errorf("failed to get reminder: %w", err) } return &reminder, nil } // GetUnreadReminders retrieves all unread reminders func (r *RepaymentRepository) GetUnreadReminders(userID uint) ([]models.PaymentReminder, error) { var reminders []models.PaymentReminder if err := r.db.Where("user_id = ? AND is_read = ?", userID, false). Preload("Bill"). Preload("Bill.Account"). Preload("Installment"). Order("reminder_date ASC"). Find(&reminders).Error; err != nil { return nil, fmt.Errorf("failed to get unread reminders: %w", err) } return reminders, nil } // GetRemindersByDateRange retrieves reminders within a date range func (r *RepaymentRepository) GetRemindersByDateRange(startDate, endDate time.Time) ([]models.PaymentReminder, error) { var reminders []models.PaymentReminder if err := r.db.Where("reminder_date >= ? AND reminder_date <= ?", startDate, endDate). Preload("Bill"). Preload("Bill.Account"). Preload("Installment"). Order("reminder_date ASC"). Find(&reminders).Error; err != nil { return nil, fmt.Errorf("failed to get reminders by date range: %w", err) } return reminders, nil } // MarkReminderAsRead marks a reminder as read func (r *RepaymentRepository) MarkReminderAsRead(id uint) error { result := r.db.Model(&models.PaymentReminder{}).Where("id = ?", id).Update("is_read", true) if result.Error != nil { return fmt.Errorf("failed to mark reminder as read: %w", result.Error) } if result.RowsAffected == 0 { return ErrReminderNotFound } return nil } // DeleteReminder deletes a reminder func (r *RepaymentRepository) DeleteReminder(id uint) error { result := r.db.Delete(&models.PaymentReminder{}, id) if result.Error != nil { return fmt.Errorf("failed to delete reminder: %w", result.Error) } if result.RowsAffected == 0 { return ErrReminderNotFound } return nil }