362 lines
12 KiB
Go
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
|
|
} |