package feedback import ( "context" "tm/internal/company" "tm/internal/tender" "tm/pkg/logger" "tm/pkg/response" ) // FeedbackService interface defines feedback business logic methods type Service interface { Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error) Get(ctx context.Context, id string) (*FeedbackResponse, error) GetByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*FeedbackResponse, error) Delete(ctx context.Context, id string) error CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error) getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse) } // feedbackService implements FeedbackService interface type feedbackService struct { feedbackRepo Repository tenderService tender.Service companyService company.Service logger logger.Logger } // NewService creates a new feedback service func NewService(feedbackRepo Repository, tenderService tender.Service, companyService company.Service, logger logger.Logger) Service { return &feedbackService{ feedbackRepo: feedbackRepo, tenderService: tenderService, companyService: companyService, logger: logger, } } func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) { existing, err := s.feedbackRepo.GetByCustomerID(ctx, *customerID, req.Tender) if err != nil { feedback := &Feedback{ TenderID: req.Tender, FeedbackType: req.Type, CompanyID: companyID, CustomerID: customerID, } err = s.feedbackRepo.Create(ctx, feedback) if err != nil { s.logger.Error("Failed to create new feedback", map[string]interface{}{ "tender_id": req.Tender, "customer_id": *customerID, "company_id": *companyID, "error": err.Error(), }) return nil, err } tender, company := s.getDetails(ctx, req.Tender, *companyID) return feedback.ToResponse(tender, company), nil } if existing.FeedbackType == req.Type { err = s.feedbackRepo.Delete(ctx, existing.GetID()) if err != nil { s.logger.Error("Failed to delete existing feedback", map[string]interface{}{ "feedback_id": existing.GetID(), "tender_id": req.Tender, "customer_id": *customerID, "error": err.Error(), }) return nil, err } if req.Type == FeedbackTypeLike { existing.FeedbackType = "unlike" } else { existing.FeedbackType = "undislike" } tender, company := s.getDetails(ctx, req.Tender, *companyID) return existing.ToResponse(tender, company), nil } if existing.FeedbackType == FeedbackTypeLike { existing.FeedbackType = FeedbackTypeDislike } else { existing.FeedbackType = FeedbackTypeLike } err = s.feedbackRepo.Update(ctx, existing) if err != nil { s.logger.Error("Failed to update existing feedback", map[string]interface{}{ "feedback_id": existing.GetID(), "tender_id": req.Tender, "customer_id": *customerID, "error": err.Error(), }) return nil, err } tender, company := s.getDetails(ctx, req.Tender, *companyID) return existing.ToResponse(tender, company), nil } func (s *feedbackService) Get(ctx context.Context, id string) (*FeedbackResponse, error) { feedback, err := s.feedbackRepo.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to get feedback", map[string]interface{}{ "id": id, "error": err.Error(), }) return nil, err } tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(*feedback)) return feedback.ToResponse(tender, company), nil } func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error) { page, err := s.feedbackRepo.Search(ctx, criteria, pagination) if err != nil { s.logger.Error("Failed to list feedback", map[string]interface{}{ "criteria": criteria, "limit": pagination.Limit, "offset": pagination.Offset, "error": err.Error(), }) return nil, err } meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset) tenderByID, companyByID := s.loadListDetails(ctx, page.Items) result := make([]*FeedbackResponse, len(page.Items)) for i, feedback := range page.Items { var tenderResp *tenderResponse if t, ok := tenderByID[feedback.TenderID]; ok { tenderResp = t } var companyResp *companyResponse if cid := feedbackCompanyID(feedback); cid != "" { companyResp = companyByID[cid] } result[i] = feedback.ToResponse(tenderResp, companyResp) } return &FeedbackListResponse{ Feedback: result, Meta: meta, }, nil } func (s *feedbackService) Delete(ctx context.Context, id string) error { return s.feedbackRepo.Delete(ctx, id) } func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID string) (*FeedbackResponse, error) { feedback, err := s.feedbackRepo.GetByTenderID(ctx, tenderID, companyID) if err != nil { s.logger.Error("Failed to get feedback by tender ID", map[string]interface{}{ "tender_id": tenderID, "error": err.Error(), }) return nil, err } tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(*feedback)) return feedback.ToResponse(tender, company), nil } func (s *feedbackService) GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*FeedbackResponse, error) { feedback, err := s.feedbackRepo.GetByTenderIDForCustomer(ctx, tenderID, customerID) if err != nil { s.logger.Error("Failed to get feedback by tender ID for customer", map[string]interface{}{ "tender_id": tenderID, "customer_id": customerID, "error": err.Error(), }) return nil, err } tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(*feedback)) return feedback.ToResponse(tender, company), nil } func feedbackCompanyID(f Feedback) string { if f.CompanyID == nil { return "" } return *f.CompanyID } func uniqueNonEmptyStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) for _, v := range values { if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out } func collectFeedbackListIDs(items []Feedback) (tenderIDs, companyIDs []string) { tenderIDs = make([]string, 0, len(items)) companyIDs = make([]string, 0, len(items)) for _, item := range items { tenderIDs = append(tenderIDs, item.TenderID) companyIDs = append(companyIDs, feedbackCompanyID(item)) } return uniqueNonEmptyStrings(tenderIDs), uniqueNonEmptyStrings(companyIDs) } // loadListDetails batch-loads tender and company data for feedback list responses. func (s *feedbackService) loadListDetails(ctx context.Context, items []Feedback) ( map[string]*tenderResponse, map[string]*companyResponse, ) { tenderByID := make(map[string]*tenderResponse) companyByID := make(map[string]*companyResponse) if len(items) == 0 { return tenderByID, companyByID } tenderIDs, companyIDs := collectFeedbackListIDs(items) if len(tenderIDs) > 0 { tenders, err := s.tenderService.GetByIDs(ctx, tenderIDs) if err != nil { s.logger.Error("Failed to batch load tenders for feedback list", map[string]interface{}{ "count": len(tenderIDs), "error": err.Error(), }) } else { for id, t := range tenders { tenderByID[id] = tenderToFeedbackResponse(t) } } } if len(companyIDs) > 0 { companies, err := s.companyService.GetNamesByIDs(ctx, companyIDs) if err != nil { s.logger.Error("Failed to batch load companies for feedback list", map[string]interface{}{ "count": len(companyIDs), "error": err.Error(), }) } else { for id, c := range companies { companyByID[id] = companyToFeedbackResponse(c) } } } return tenderByID, companyByID } func tenderToFeedbackResponse(t *tender.TenderResponse) *tenderResponse { if t == nil { return nil } var buyer *organizationResponse if t.BuyerOrganization != nil { buyer = &organizationResponse{Name: t.BuyerOrganization.Name} } return &tenderResponse{ ID: t.ID, NoticePublicationID: t.NoticePublicationID, PublicationDate: t.PublicationDate, Title: t.Title, Description: t.Description, ProcurementTypeCode: t.ProcurementTypeCode, ProcedureCode: t.ProcedureCode, EstimatedValue: t.EstimatedValue, Currency: t.Currency, Duration: t.Duration, DurationUnit: t.DurationUnit, TenderDeadline: t.TenderDeadline, SubmissionDeadline: t.SubmissionDeadline, CountryCode: t.CountryCode, BuyerOrganization: buyer, Status: string(t.Status), } } func companyToFeedbackResponse(c *company.CompanyResponse) *companyResponse { if c == nil { return nil } return &companyResponse{ ID: c.ID, Name: c.Name, } } func (s *feedbackService) getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse) { var tenderResp *tenderResponse tender, err := s.tenderService.GetByID(ctx, tenderID, "") if err != nil { s.logger.Error("Failed to get tender", map[string]interface{}{ "tender_id": tenderID, "error": err.Error(), }) } else { tenderResp = tenderToFeedbackResponse(tender) } var companyResp *companyResponse if companyID != "" { company, err := s.companyService.GetByID(ctx, companyID) if err != nil { s.logger.Error("Failed to get company", map[string]interface{}{ "company_id": companyID, "error": err.Error(), }) } else { companyResp = companyToFeedbackResponse(company) } } return tenderResp, companyResp } func (s *feedbackService) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) { s.logger.Info("Getting company feedback statistics", map[string]interface{}{ "company_id": companyID, }) stats, err := s.feedbackRepo.CompanyStats(ctx, companyID) if err != nil { s.logger.Error("Failed to get company stats", map[string]interface{}{ "company_id": companyID, "error": err.Error(), }) return nil, err } s.logger.Info("Company stats retrieved successfully", map[string]interface{}{ "company_id": companyID, "total_feedback": stats.TotalFeedback, "total_likes": stats.TotalLikes, "total_dislikes": stats.TotalDislikes, }) return stats, nil } func (s *feedbackService) CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error) { s.logger.Info("Getting customer feedback statistics", map[string]interface{}{ "customer_id": customerID, }) stats, err := s.feedbackRepo.CustomerStats(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer stats", map[string]interface{}{ "customer_id": customerID, "error": err.Error(), }) return nil, err } s.logger.Info("Customer stats retrieved successfully", map[string]interface{}{ "customer_id": customerID, "total_feedback": stats.TotalFeedback, "total_likes": stats.TotalLikes, "total_dislikes": stats.TotalDislikes, }) return stats, nil }