615 lines
20 KiB
Go
615 lines
20 KiB
Go
// internal/services/item_service.go - FIXED VERSION
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"lost-and-found/internal/models"
|
|
"lost-and-found/internal/repositories"
|
|
"time"
|
|
"log"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type ItemService struct {
|
|
itemRepo *repositories.ItemRepository
|
|
categoryRepo *repositories.CategoryRepository
|
|
auditLogRepo *repositories.AuditLogRepository
|
|
revisionRepo *repositories.RevisionLogRepository
|
|
db *gorm.DB
|
|
lostItemRepo *repositories.LostItemRepository // FIXED: pointer type
|
|
notificationRepo *repositories.NotificationRepository // FIXED: pointer type
|
|
matchRepo *repositories.MatchResultRepository // TAMBAHAN
|
|
}
|
|
|
|
func NewItemService(db *gorm.DB) *ItemService {
|
|
itemRepo := repositories.NewItemRepository(db)
|
|
lostItemRepo := repositories.NewLostItemRepository(db)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
matchRepo := repositories.NewMatchResultRepository(db)
|
|
|
|
return &ItemService{
|
|
db: db,
|
|
itemRepo: itemRepo,
|
|
categoryRepo: repositories.NewCategoryRepository(db),
|
|
auditLogRepo: repositories.NewAuditLogRepository(db),
|
|
revisionRepo: repositories.NewRevisionLogRepository(db),
|
|
lostItemRepo: lostItemRepo,
|
|
notificationRepo: notifRepo,
|
|
matchRepo: matchRepo,
|
|
}
|
|
}
|
|
|
|
func (s *ItemService) CreateFoundItemLinked(reporterID uint, req CreateFoundItemLinkedRequest, ipAddress, userAgent string) (*models.Item, error) {
|
|
tx := s.db.Begin()
|
|
if tx.Error != nil {
|
|
return nil, tx.Error
|
|
}
|
|
|
|
// 1. Buat Item Awal (Status Default: unclaimed)
|
|
item := &models.Item{
|
|
Name: req.Name,
|
|
CategoryID: req.CategoryID,
|
|
Location: req.Location,
|
|
DateFound: time.Now(),
|
|
Description: req.Description,
|
|
ReporterID: reporterID,
|
|
ReporterName: req.ReporterName,
|
|
ReporterContact: req.ReporterContact,
|
|
Status: models.ItemStatusUnclaimed, // Default
|
|
}
|
|
|
|
if req.PhotoURL != "" {
|
|
item.PhotoURL = req.PhotoURL
|
|
}
|
|
|
|
if err := tx.Create(item).Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Logika "Langsung ke Pemilik"
|
|
// Pastikan req.LostItemID & req.IsDirectToOwner terisi (Cek tag JSON struct!)
|
|
if req.IsDirectToOwner && req.LostItemID != 0 {
|
|
var lostItem models.LostItem
|
|
// Gunakan Preload agar data User tidak nil saat dipakai nanti
|
|
if err := tx.Preload("User").First(&lostItem, req.LostItemID).Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, errors.New("Laporan kehilangan tidak ditemukan")
|
|
}
|
|
|
|
// A. Update Status Lost Item jadi "claimed"
|
|
if err := tx.Model(&lostItem).Update("status", "claimed").Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
// B. Update Status Item Temuan jadi "waiting_owner"
|
|
if err := tx.Model(item).Update("status", "waiting_owner").Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
// ✅ PENTING: Update struct di memori supaya response API benar
|
|
itemID := item.ID
|
|
|
|
newClaim := &models.Claim{
|
|
ItemID: &itemID, // PENTING: Harus pointer
|
|
UserID: lostItem.UserID,
|
|
Description: "Auto-match: Ditemukan dan diserahkan langsung ke pemilik.",
|
|
Contact: lostItem.User.Phone,
|
|
Status: models.ClaimStatusWaitingOwner,
|
|
}
|
|
|
|
if err := tx.Create(newClaim).Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
type CreateItemRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
CategoryID uint `json:"category_id" binding:"required"`
|
|
PhotoURL string `json:"photo_url"`
|
|
Location string `json:"location" binding:"required"`
|
|
Description string `json:"description" binding:"required"`
|
|
SecretDetails string `json:"secret_details" binding:"required"`
|
|
DateFound time.Time `json:"date_found" binding:"required"`
|
|
ReporterName string `json:"reporter_name" binding:"required"`
|
|
ReporterContact string `json:"reporter_contact" binding:"required"`
|
|
}
|
|
|
|
type UpdateItemRequest struct {
|
|
Name string `json:"name"`
|
|
CategoryID uint `json:"category_id"`
|
|
PhotoURL string `json:"photo_url"`
|
|
Location string `json:"location"`
|
|
Description string `json:"description"`
|
|
SecretDetails string `json:"secret_details"`
|
|
DateFound time.Time `json:"date_found"`
|
|
ReporterName string `json:"reporter_name"`
|
|
ReporterContact string `json:"reporter_contact"`
|
|
Status string `json:"status"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type CreateFoundItemLinkedRequest struct {
|
|
CreateItemRequest
|
|
LostItemID uint `json:"lost_item_id" binding:"required"`
|
|
IsDirectToOwner bool `json:"is_direct_to_owner"`
|
|
}
|
|
|
|
// GetAllItems gets all items with CONTEXT TIMEOUT
|
|
func (s *ItemService) GetAllItems(page, limit int, status, category, search string) ([]models.ItemPublicResponse, int64, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
txRepo := repositories.NewItemRepository(s.db.WithContext(ctx))
|
|
items, total, err := txRepo.FindAll(page, limit, status, category, search)
|
|
if err != nil {
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return nil, 0, errors.New("request timeout: query took too long")
|
|
}
|
|
return nil, 0, err
|
|
}
|
|
|
|
var responses []models.ItemPublicResponse
|
|
for _, item := range items {
|
|
responses = append(responses, item.ToPublicResponse())
|
|
}
|
|
|
|
return responses, total, nil
|
|
}
|
|
|
|
func (s *ItemService) RunAutoArchive(ipAddress, userAgent string) (int, error) {
|
|
// Panggil Repository
|
|
count, err := s.itemRepo.CallArchiveExpiredProcedure()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Log Audit jika ada yang diarsip
|
|
if count > 0 {
|
|
details := fmt.Sprintf("Auto-archived %d expired items using Stored Procedure", count)
|
|
// Gunakan ID 0 atau nil untuk system action
|
|
s.auditLogRepo.Log(nil, "auto_archive", "system", nil, details, ipAddress, userAgent)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// ✅ FIXED: CreateItem - NOW INCLUDES SecretDetails
|
|
func (s *ItemService) CreateItem(reporterID uint, req CreateItemRequest, ipAddress, userAgent string) (*models.Item, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var item *models.Item
|
|
|
|
// ✅ TRANSACTION untuk create item + audit log
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 1. Verify category exists
|
|
var category models.Category
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", req.CategoryID).
|
|
First(&category).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errors.New("invalid category")
|
|
}
|
|
return fmt.Errorf("failed to verify category: %w", err)
|
|
}
|
|
|
|
// 2. Create item - ✅ SEKARANG INCLUDE SecretDetails
|
|
item = &models.Item{
|
|
Name: req.Name,
|
|
CategoryID: req.CategoryID,
|
|
PhotoURL: req.PhotoURL,
|
|
Location: req.Location,
|
|
Description: req.Description,
|
|
SecretDetails: req.SecretDetails, // ✅ TAMBAHKAN INI
|
|
DateFound: req.DateFound,
|
|
Status: models.ItemStatusUnclaimed,
|
|
ReporterID: reporterID,
|
|
ReporterName: req.ReporterName,
|
|
ReporterContact: req.ReporterContact,
|
|
}
|
|
|
|
if err := tx.Create(item).Error; err != nil {
|
|
return fmt.Errorf("failed to create item: %w", err)
|
|
}
|
|
|
|
// 3. Create audit log
|
|
auditLog := &models.AuditLog{
|
|
UserID: &reporterID,
|
|
Action: models.ActionCreate,
|
|
EntityType: models.EntityItem,
|
|
EntityID: &item.ID,
|
|
Details: fmt.Sprintf("Item created: %s", item.Name),
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
}
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
// ✅ FIXED: UpdateItem - NOW HANDLES SecretDetails
|
|
// internal/services/item_service.go
|
|
|
|
// UpdateItem updates an item with transaction, locking, and status change support
|
|
func (s *ItemService) UpdateItem(userID, itemID uint, req UpdateItemRequest, ipAddress, userAgent string) (*models.Item, error) {
|
|
// ✅ Tambahkan logging
|
|
log.Printf("🔍 UpdateItem Request:")
|
|
log.Printf(" ItemID: %d", itemID)
|
|
log.Printf(" Name: %s", req.Name)
|
|
log.Printf(" CategoryID: %d", req.CategoryID)
|
|
log.Printf(" Status: %s", req.Status)
|
|
log.Printf(" Location: %s", req.Location)
|
|
log.Printf(" Description: %s", req.Description)
|
|
log.Printf(" SecretDetails: %s", req.SecretDetails)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var updatedItem *models.Item
|
|
|
|
// ✅ TRANSACTION + LOCKING untuk update item
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var item models.Item
|
|
|
|
// 1. Ambil data User beserta Role-nya (FIXED: Preload Role)
|
|
var user models.User
|
|
if err := tx.Preload("Role").First(&user, userID).Error; err != nil {
|
|
return fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// 2. Lock item untuk mencegah race condition saat update
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Preload("Category").
|
|
Where("id = ? AND deleted_at IS NULL", itemID).
|
|
First(&item).Error; err != nil {
|
|
return errors.New("item not found")
|
|
}
|
|
|
|
// 3. Cek Permission: Izinkan jika User adalah Owner ATAU Admin/Manager
|
|
isOwner := item.ReporterID == userID
|
|
isManagerOrAdmin := user.Role.Name == "admin" || user.Role.Name == "manager"
|
|
|
|
if !isOwner && !isManagerOrAdmin {
|
|
return errors.New("unauthorized to edit this item")
|
|
}
|
|
|
|
// 4. Validasi Status Item
|
|
// - Case Closed tetap permanen (tidak bisa diedit siapapun)
|
|
if item.Status == models.ItemStatusCaseClosed {
|
|
return errors.New("cannot edit item with status: " + item.Status)
|
|
}
|
|
|
|
// - Expired hanya bisa diedit oleh Manager/Admin
|
|
if !isManagerOrAdmin && item.IsExpired() {
|
|
return errors.New("cannot edit expired item")
|
|
}
|
|
|
|
// Track changes for revision log
|
|
revisionCreated := false
|
|
|
|
// --- Update Fields Logics ---
|
|
|
|
if req.Name != "" && req.Name != item.Name {
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "name",
|
|
OldValue: item.Name,
|
|
NewValue: req.Name,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.Name = req.Name
|
|
revisionCreated = true
|
|
}
|
|
|
|
if req.CategoryID != 0 && req.CategoryID != item.CategoryID {
|
|
// Verify new category exists
|
|
var newCategory models.Category
|
|
if err := tx.Where("id = ?", req.CategoryID).First(&newCategory).Error; err != nil {
|
|
return errors.New("invalid category")
|
|
}
|
|
|
|
oldCatName := item.Category.Name
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "category",
|
|
OldValue: oldCatName,
|
|
NewValue: newCategory.Name,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.CategoryID = req.CategoryID
|
|
revisionCreated = true
|
|
}
|
|
|
|
if req.Location != "" && req.Location != item.Location {
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "location",
|
|
OldValue: item.Location,
|
|
NewValue: req.Location,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.Location = req.Location
|
|
revisionCreated = true
|
|
}
|
|
|
|
if req.Description != "" && req.Description != item.Description {
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "description",
|
|
OldValue: item.Description,
|
|
NewValue: req.Description,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.Description = req.Description
|
|
revisionCreated = true
|
|
}
|
|
|
|
// Handle SecretDetails update
|
|
if req.SecretDetails != "" && req.SecretDetails != item.SecretDetails {
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "secret_details",
|
|
OldValue: item.SecretDetails,
|
|
NewValue: req.SecretDetails,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.SecretDetails = req.SecretDetails
|
|
revisionCreated = true
|
|
}
|
|
|
|
if req.PhotoURL != "" && req.PhotoURL != item.PhotoURL {
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "photo_url",
|
|
OldValue: item.PhotoURL,
|
|
NewValue: req.PhotoURL,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log: %w", err)
|
|
}
|
|
item.PhotoURL = req.PhotoURL
|
|
revisionCreated = true
|
|
}
|
|
|
|
// ✅ NEW: Handle Status Update (Khusus Manager/Admin)
|
|
if req.Status != "" && req.Status != item.Status {
|
|
// Validasi status yang diperbolehkan untuk di-set manual
|
|
validStatuses := map[string]bool{
|
|
models.ItemStatusUnclaimed: true,
|
|
models.ItemStatusVerified: true,
|
|
models.ItemStatusExpired: true,
|
|
models.ItemStatusCaseClosed: true,
|
|
}
|
|
|
|
if !validStatuses[req.Status] {
|
|
return errors.New("invalid status value")
|
|
}
|
|
|
|
revLog := &models.RevisionLog{
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
FieldName: "status",
|
|
OldValue: item.Status,
|
|
NewValue: req.Status,
|
|
Reason: req.Reason,
|
|
}
|
|
if err := tx.Create(revLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create revision log for status: %w", err)
|
|
}
|
|
item.Status = req.Status
|
|
revisionCreated = true
|
|
}
|
|
|
|
if !revisionCreated {
|
|
return errors.New("no changes detected")
|
|
}
|
|
|
|
// Save updated item
|
|
if err := tx.Save(&item).Error; err != nil {
|
|
return fmt.Errorf("failed to update item: %w", err)
|
|
}
|
|
|
|
// Create audit log
|
|
auditLog := &models.AuditLog{
|
|
UserID: &userID,
|
|
Action: models.ActionUpdate,
|
|
EntityType: models.EntityItem,
|
|
EntityID: &itemID,
|
|
Details: fmt.Sprintf("Item updated: %s (Reason: %s)", item.Name, req.Reason),
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
}
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
|
|
updatedItem = &item
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updatedItem, nil
|
|
}
|
|
|
|
// UpdateItemStatus updates item status with TRANSACTION
|
|
func (s *ItemService) UpdateItemStatus(userID, itemID uint, status string, ipAddress, userAgent string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// Lock item
|
|
var item models.Item
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("id = ?", itemID).
|
|
First(&item).Error; err != nil {
|
|
return fmt.Errorf("failed to lock item: %w", err)
|
|
}
|
|
|
|
// Update status
|
|
if err := tx.Model(&item).Update("status", status).Error; err != nil {
|
|
return fmt.Errorf("failed to update status: %w", err)
|
|
}
|
|
|
|
// Create audit log
|
|
auditLog := &models.AuditLog{
|
|
UserID: &userID,
|
|
Action: models.ActionUpdate,
|
|
EntityType: models.EntityItem,
|
|
EntityID: &itemID,
|
|
Details: fmt.Sprintf("Item status updated to: %s", status),
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
}
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// DeleteItem deletes an item with TRANSACTION
|
|
func (s *ItemService) DeleteItem(userID, itemID uint, ipAddress, userAgent string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 1. Ambil data User yang sedang request (untuk cek Role)
|
|
var user models.User
|
|
if err := tx.Preload("Role").First(&user, userID).Error; err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// 2. Lock item
|
|
var item models.Item
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("id = ?", itemID).
|
|
First(&item).Error; err != nil {
|
|
return fmt.Errorf("failed to lock item: %w", err)
|
|
}
|
|
|
|
// 3. PERMISSION CHECK: Izinkan jika Owner ATAU Manager/Admin
|
|
isOwner := item.ReporterID == userID
|
|
isManagerOrAdmin := user.Role.Name == "admin" || user.Role.Name == "manager"
|
|
|
|
if !isOwner && !isManagerOrAdmin {
|
|
return errors.New("unauthorized to delete this item")
|
|
}
|
|
|
|
// 4. ✅ (BARU) Validasi Active Claims - PENGGANTI TRIGGER trg_items_before_delete
|
|
// Cek apakah ada claims dengan status 'pending' untuk item ini
|
|
var activeClaims int64
|
|
if err := tx.Model(&models.Claim{}).
|
|
Where("item_id = ? AND status IN ? AND deleted_at IS NULL",
|
|
itemID, []string{models.ClaimStatusPending, models.ClaimStatusWaitingOwner}).
|
|
Count(&activeClaims).Error; err != nil {
|
|
return fmt.Errorf("failed to check active claims: %w", err)
|
|
}
|
|
|
|
if activeClaims > 0 {
|
|
return errors.New("cannot delete item with active claims (pending or waiting owner)")
|
|
}
|
|
|
|
if item.Status == models.ItemStatusVerified || item.Status == models.ItemStatusCaseClosed {
|
|
return errors.New("cannot delete item with status: " + item.Status)
|
|
}
|
|
|
|
if err := tx.Delete(&item).Error; err != nil {
|
|
return fmt.Errorf("failed to delete item: %w", err)
|
|
}
|
|
|
|
// 7. Create audit log
|
|
auditLog := &models.AuditLog{
|
|
UserID: &userID,
|
|
Action: models.ActionDelete,
|
|
EntityType: models.EntityItem,
|
|
EntityID: &itemID,
|
|
Details: fmt.Sprintf("Item deleted: %s", item.Name),
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
}
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
return fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GetItemsByReporter gets items by reporter with CONTEXT
|
|
func (s *ItemService) GetItemsByReporter(reporterID uint, page, limit int) ([]models.Item, int64, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
txRepo := repositories.NewItemRepository(s.db.WithContext(ctx))
|
|
return txRepo.FindByReporter(reporterID, page, limit)
|
|
}
|
|
|
|
// GetItemRevisionHistory gets revision history with CONTEXT
|
|
func (s *ItemService) GetItemRevisionHistory(itemID uint, page, limit int) ([]models.RevisionLogResponse, int64, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
txRepo := repositories.NewRevisionLogRepository(s.db.WithContext(ctx))
|
|
logs, total, err := txRepo.FindByItem(itemID, page, limit)
|
|
if err != nil {
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return nil, 0, errors.New("request timeout")
|
|
}
|
|
return nil, 0, err
|
|
}
|
|
|
|
var responses []models.RevisionLogResponse
|
|
for _, log := range logs {
|
|
responses = append(responses, log.ToResponse())
|
|
}
|
|
|
|
return responses, total, nil
|
|
} |