package services import ( "errors" "lost-and-found/internal/models" "lost-and-found/internal/repositories" "time" "fmt" "gorm.io/gorm" ) type LostItemService struct { db *gorm.DB lostItemRepo *repositories.LostItemRepository categoryRepo *repositories.CategoryRepository auditLogRepo *repositories.AuditLogRepository userRepo *repositories.UserRepository notificationRepo *repositories.NotificationRepository claimRepo *repositories.ClaimRepository } func NewLostItemService(db *gorm.DB) *LostItemService { return &LostItemService{ db: db, lostItemRepo: repositories.NewLostItemRepository(db), categoryRepo: repositories.NewCategoryRepository(db), auditLogRepo: repositories.NewAuditLogRepository(db), userRepo: repositories.NewUserRepository(db), notificationRepo: repositories.NewNotificationRepository(db), claimRepo: repositories.NewClaimRepository(db), } } type CreateLostItemRequest struct { Name string `json:"name" binding:"required"` CategoryID uint `json:"category_id" binding:"required"` Color string `json:"color"` Location string `json:"location"` Description string `json:"description" binding:"required"` DateLost time.Time `json:"date_lost" binding:"required"` } type UpdateLostItemRequest struct { Name string `json:"name"` CategoryID uint `json:"category_id"` Color string `json:"color"` Location string `json:"location"` Description string `json:"description"` DateLost time.Time `json:"date_lost"` } type CreateLostItemClaimRequest struct { Description string `json:"description" binding:"required"` Contact string `json:"contact" binding:"required"` ProofURL string `json:"proof_url"` } func (s *LostItemService) GetAllLostItems(page, limit int, status, category, search string, userID *uint) ([]models.LostItemResponse, int64, error) { lostItems, total, err := s.lostItemRepo.FindAll(page, limit, status, category, search, userID) if err != nil { return nil, 0, err } var responses []models.LostItemResponse for _, lostItem := range lostItems { response := lostItem.ToResponse() if lostItem.DirectClaimID != nil { claim, err := s.claimRepo.FindByID(*lostItem.DirectClaimID) if err == nil { response.DirectClaimStatus = claim.Status } } responses = append(responses, response) } return responses, total, nil } func (s *LostItemService) GetLostItemByID(id uint) (*models.LostItem, error) { lostItem, err := s.lostItemRepo.FindByID(id) if err != nil { return nil, err } if lostItem.DirectClaimID != nil { claim, err := s.claimRepo.FindByID(*lostItem.DirectClaimID) if err == nil { lostItem.DirectClaim = claim } } return lostItem, nil } func (s *LostItemService) CreateLostItem(userID uint, req CreateLostItemRequest, ipAddress, userAgent string) (*models.LostItem, error) { if _, err := s.categoryRepo.FindByID(req.CategoryID); err != nil { return nil, errors.New("invalid category") } lostItem := &models.LostItem{ UserID: userID, Name: req.Name, CategoryID: req.CategoryID, Color: req.Color, Location: req.Location, Description: req.Description, DateLost: req.DateLost, Status: models.LostItemStatusActive, } if err := s.lostItemRepo.Create(lostItem); err != nil { return nil, errors.New("failed to create lost item report") } s.auditLogRepo.Log(&userID, models.ActionCreate, models.EntityLostItem, &lostItem.ID, "Lost item report created: "+lostItem.Name, ipAddress, userAgent) return s.lostItemRepo.FindByID(lostItem.ID) } func (s *LostItemService) UpdateLostItem(userID, lostItemID uint, req UpdateLostItemRequest, ipAddress, userAgent string) (*models.LostItem, error) { lostItem, err := s.lostItemRepo.FindByID(lostItemID) if err != nil { return nil, err } // Cek authorization user, err := s.userRepo.FindByID(userID) if err != nil { return nil, errors.New("user not found") } isOwner := lostItem.UserID == userID isAdminOrManager := user.Role.Name == models.RoleAdmin || user.Role.Name == models.RoleManager if !isOwner && !isAdminOrManager { return nil, errors.New("unauthorized to update this lost item report") } if !isAdminOrManager && !lostItem.IsActive() { return nil, errors.New("cannot update non-active lost item report") } // ✅ UPDATE: Prioritaskan category_id, update terlebih dahulu if req.CategoryID != 0 { if _, err := s.categoryRepo.FindByID(req.CategoryID); err != nil { return nil, errors.New("invalid category") } // LOG untuk debug fmt.Printf("📝 Updating category from %d to %d\n", lostItem.CategoryID, req.CategoryID) lostItem.CategoryID = req.CategoryID } if req.Name != "" { lostItem.Name = req.Name } if req.Color != "" { lostItem.Color = req.Color } if req.Location != "" { lostItem.Location = req.Location } if req.Description != "" { lostItem.Description = req.Description } if !req.DateLost.IsZero() { lostItem.DateLost = req.DateLost } // ✅ PASTIKAN update tersimpan if err := s.lostItemRepo.Update(lostItem); err != nil { fmt.Printf("❌ Error updating lost item: %v\n", err) return nil, errors.New("failed to update lost item report") } actionDesc := fmt.Sprintf("Lost item report updated: %s", lostItem.Name) if !isOwner { actionDesc = fmt.Sprintf("Lost item report updated by %s: %s", user.Role.Name, lostItem.Name) } s.auditLogRepo.Log(&userID, models.ActionUpdate, models.EntityLostItem, &lostItemID, actionDesc, ipAddress, userAgent) // ✅ RELOAD dari database untuk memastikan data terbaru return s.lostItemRepo.FindByID(lostItem.ID) } func (s *LostItemService) UpdateLostItemStatus(userID, lostItemID uint, status string, ipAddress, userAgent string) error { lostItem, err := s.lostItemRepo.FindByID(lostItemID) if err != nil { return err } // TAMBAHKAN: Cek admin/manager permission user, err := s.userRepo.FindByID(userID) if err != nil { return errors.New("user not found") } isOwner := lostItem.UserID == userID isAdminOrManager := user.Role.Name == models.RoleAdmin || user.Role.Name == models.RoleManager if !isOwner && !isAdminOrManager { return errors.New("unauthorized to update this lost item report") } if err := s.lostItemRepo.UpdateStatus(lostItemID, status); err != nil { return errors.New("failed to update lost item status") } s.auditLogRepo.Log(&userID, models.ActionUpdate, models.EntityLostItem, &lostItemID, "Lost item status updated to: "+status, ipAddress, userAgent) return nil } func (s *LostItemService) DeleteLostItem(userID, lostItemID uint, ipAddress, userAgent string) error { lostItem, err := s.lostItemRepo.FindByID(lostItemID) if err != nil { return err } user, err := s.userRepo.FindByID(userID) if err != nil { return errors.New("user not found") } isOwner := lostItem.UserID == userID isManagerOrAdmin := user.Role.Name == models.RoleAdmin || user.Role.Name == models.RoleManager if !isOwner && !isManagerOrAdmin { return errors.New("unauthorized to delete this lost item report") } if err := s.lostItemRepo.Delete(lostItemID); err != nil { return errors.New("failed to delete lost item report") } s.auditLogRepo.Log(&userID, models.ActionDelete, models.EntityLostItem, &lostItemID, "Lost item report deleted: "+lostItem.Name, ipAddress, userAgent) return nil } func (s *LostItemService) GetLostItemsByUser(userID uint, page, limit int) ([]models.LostItemResponse, int64, error) { lostItems, total, err := s.lostItemRepo.FindByUser(userID, page, limit) if err != nil { return nil, 0, err } var responses []models.LostItemResponse for _, lostItem := range lostItems { response := lostItem.ToResponse() // ✅ PERBAIKAN: Load data claim lengkap jika ada direct claim if lostItem.DirectClaimID != nil { claim, err := s.claimRepo.FindByID(*lostItem.DirectClaimID) if err == nil { // Load relasi user penemu agar namanya muncul di frontend s.db.Preload("User").First(claim, claim.ID) response.DirectClaimStatus = claim.Status // Konversi claim ke response dan tempelkan ke lost item response claimResp := claim.ToResponse() response.DirectClaim = &claimResp // Pastikan struct LostItemResponse punya field ini } } responses = append(responses, response) } return responses, total, nil } func (s *LostItemService) DirectClaimToOwner( finderUserID uint, lostItemID uint, req CreateLostItemClaimRequest, ipAddress string, // Tambahkan ini agar audit log valid userAgent string, // Tambahkan ini agar audit log valid ) (*models.Claim, error) { var createdClaim *models.Claim err := s.db.Transaction(func(tx *gorm.DB) error { // 1. Get Lost Item var lostItem models.LostItem if err := tx.First(&lostItem, lostItemID).Error; err != nil { return errors.New("lost item not found") } // 2. Validasi if lostItem.Status != models.LostItemStatusActive { return errors.New("lost item is not active") } if lostItem.UserID == finderUserID { return errors.New("cannot claim your own lost item") } if lostItem.DirectClaimID != nil { return errors.New("this lost item already has a pending claim") } // 3. Create Claim (LOGIC OPSI A) // Kita set ItemID nil, tapi isi LostItemID claim := models.Claim{ ItemID: nil, // PENTING: Set nil (jangan 0) LostItemID: &lostItemID, // PENTING: Link ke LostItem UserID: finderUserID, Description: req.Description, Contact: req.Contact, ProofURL: req.ProofURL, Status: models.ClaimStatusWaitingOwner, Notes: fmt.Sprintf("Direct claim for lost item #%d", lostItemID), } if err := tx.Create(&claim).Error; err != nil { return err } createdClaim = &claim // 4. Update Lost Item Status // Status diganti agar tidak muncul di pencarian publik sementara waktu // Pastikan constant ini ada di models, atau gunakan string "pending_verification" lostItem.Status = "pending_verification" lostItem.DirectClaimID = &claim.ID if err := tx.Save(&lostItem).Error; err != nil { return err } // 5. Notification to Owner s.notificationRepo.Notify( lostItem.UserID, "direct_claim_received", "🎯 Barang Anda Ditemukan?", fmt.Sprintf("Seseorang mengklaim menemukan '%s'. Silakan verifikasi klaim ini.", lostItem.Name), "claim", // Entity Type arahkan ke claim &claim.ID, // Entity ID arahkan ke claim ID agar owner bisa klik detail claim ) // 6. Audit Log s.auditLogRepo.Log( &finderUserID, "create_direct_claim", "claim", &claim.ID, fmt.Sprintf("Direct claim created for lost item #%d", lostItemID), ipAddress, userAgent, ) return nil }) if err != nil { return nil, err } return createdClaim, nil }