Files
tm_back/internal/feedback/service.go
T
2026-05-25 11:27:55 +03:30

349 lines
9.8 KiB
Go

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)
Delete(ctx context.Context, id string) error
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, 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.GetByCompanyID(ctx, *companyID, 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,
"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 update existing feedback", map[string]interface{}{
"feedback_id": existing.GetID(),
"tender_id": req.Tender,
"company_id": *companyID,
"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,
"company_id": *companyID,
"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 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
}