Basdat/internal/services/lost_item_service.go
2025-12-20 00:01:08 +07:00

362 lines
12 KiB
Go

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
}