Merge branch 'develop' into TM-571
This commit is contained in:
@@ -204,6 +204,7 @@ func main() {
|
|||||||
userValidator := user.NewValidationService()
|
userValidator := user.NewValidationService()
|
||||||
customerValidator := customer.NewValidationService()
|
customerValidator := customer.NewValidationService()
|
||||||
_ = kanban.NewValidationService() // Register hexcolor validator
|
_ = kanban.NewValidationService() // Register hexcolor validator
|
||||||
|
_ = cms.NewValidationService() // Register mediaRef validator
|
||||||
|
|
||||||
// Initialize services with repositories
|
// Initialize services with repositories
|
||||||
auditLogger := audit.NewLogger(logger)
|
auditLogger := audit.NewLogger(logger)
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
|
|||||||
feedbackGP.GET("", feedbackHandler.PublicList)
|
feedbackGP.GET("", feedbackHandler.PublicList)
|
||||||
feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetByTenderID)
|
feedbackGP.GET("/tender/:tender_id", feedbackHandler.PublicGetByTenderID)
|
||||||
feedbackGP.GET("/:id", feedbackHandler.PublicGet)
|
feedbackGP.GET("/:id", feedbackHandler.PublicGet)
|
||||||
|
feedbackGP.GET("/stats/customer", feedbackHandler.GetCustomerStats)
|
||||||
feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyStats)
|
feedbackGP.GET("/stats/company", feedbackHandler.GetCompanyStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type CMSForm struct {
|
|||||||
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
|
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
|
||||||
Language string `json:"language" valid:"optional,length(2|100)" example:"en"`
|
Language string `json:"language" valid:"optional,length(2|100)" example:"en"`
|
||||||
CompanyName string `json:"company_name" valid:"optional,length(2|100)" example:"Opplens"`
|
CompanyName string `json:"company_name" valid:"optional,length(2|100)" example:"Opplens"`
|
||||||
Country string `json:"country" valid:"optional,length(3|3)" example:"SWE"`
|
Country string `json:"country" valid:"optional,length(2|3)" example:"SWE"`
|
||||||
Type CMSType `json:"type" valid:"optional,in(company|organization)" example:"company"`
|
Type CMSType `json:"type" valid:"optional,in(company|organization)" example:"company"`
|
||||||
Hero heroSectionForm `json:"hero" valid:"optional"`
|
Hero heroSectionForm `json:"hero" valid:"optional"`
|
||||||
Chart chartSectionForm `json:"chart" valid:"optional"`
|
Chart chartSectionForm `json:"chart" valid:"optional"`
|
||||||
@@ -24,7 +24,7 @@ type heroSectionForm struct {
|
|||||||
Description string `json:"description" valid:"optional,length(2|1000)" example:"Hero Description"`
|
Description string `json:"description" valid:"optional,length(2|1000)" example:"Hero Description"`
|
||||||
ButtonText string `json:"button_text" valid:"optional,length(2|100)" example:"Button Text"`
|
ButtonText string `json:"button_text" valid:"optional,length(2|100)" example:"Button Text"`
|
||||||
ButtonLink string `json:"button_link" valid:"optional,url" example:"https://example.com"`
|
ButtonLink string `json:"button_link" valid:"optional,url" example:"https://example.com"`
|
||||||
GifFile string `json:"gif_file" valid:"optional,url" example:"https://example.com/gif.gif"`
|
GifFile string `json:"gif_file" valid:"optional,mediaRef" example:"674abc123def456789012345"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// chartSectionForm represents the form for creating/updating the chart section
|
// chartSectionForm represents the form for creating/updating the chart section
|
||||||
@@ -49,7 +49,7 @@ type cardSectionForm struct {
|
|||||||
|
|
||||||
// cardForm represents the form for creating/updating a card
|
// cardForm represents the form for creating/updating a card
|
||||||
type cardForm struct {
|
type cardForm struct {
|
||||||
Icon string `json:"icon" valid:"optional,url" example:"https://example.com/icon.png"`
|
Icon string `json:"icon" valid:"optional,mediaRef" example:"674abc123def456789012345"`
|
||||||
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
|
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
|
||||||
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
|
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ type formFieldForm struct {
|
|||||||
// footerSectionForm represents the form for creating/updating the footer section
|
// footerSectionForm represents the form for creating/updating the footer section
|
||||||
type footerSectionForm struct {
|
type footerSectionForm struct {
|
||||||
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
|
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
|
||||||
Phone string `json:"phone" valid:"optional,length(10|20)" example:"+1234567890"`
|
Phone string `json:"phone" valid:"optional,length(0|30)" example:"+1234567890"`
|
||||||
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
|
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
|
||||||
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
|
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
|
||||||
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
|
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
|
||||||
|
|||||||
+12
-29
@@ -53,21 +53,17 @@ func (h *Handler) GetByKey(c echo.Context) error {
|
|||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /admin/v1/cms [post]
|
// @Router /admin/v1/cms [post]
|
||||||
func (h *Handler) Create(c echo.Context) error {
|
func (h *Handler) Create(c echo.Context) error {
|
||||||
form, err := response.Parse[CMSForm](c)
|
form, err := ParseForm(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
return handleParseError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CMS
|
|
||||||
cms, err := h.service.Create(c.Request().Context(), form)
|
cms, err := h.service.Create(c.Request().Context(), form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrCMSKeyExists {
|
return handleServiceError(c, err, "Failed to create marketing page")
|
||||||
return response.Conflict(c, "CMS with this key already exists")
|
|
||||||
}
|
|
||||||
return response.InternalServerError(c, "Failed to create CMS")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Created(c, cms, "CMS entry created successfully")
|
return response.Created(c, cms, "Marketing page created successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByID handles retrieving a CMS entry by ID (admin only)
|
// GetByID handles retrieving a CMS entry by ID (admin only)
|
||||||
@@ -86,13 +82,10 @@ func (h *Handler) GetByID(c echo.Context) error {
|
|||||||
|
|
||||||
cms, err := h.service.GetByID(c.Request().Context(), id)
|
cms, err := h.service.GetByID(c.Request().Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "cms not found" {
|
return handleServiceError(c, err, "Failed to get marketing page")
|
||||||
return response.NotFound(c, "CMS entry not found")
|
|
||||||
}
|
|
||||||
return response.InternalServerError(c, "Failed to get CMS entry")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, cms, "CMS entry retrieved successfully")
|
return response.Success(c, cms, "Marketing page retrieved successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles updating a CMS entry (admin only)
|
// Update handles updating a CMS entry (admin only)
|
||||||
@@ -112,24 +105,17 @@ func (h *Handler) GetByID(c echo.Context) error {
|
|||||||
func (h *Handler) Update(c echo.Context) error {
|
func (h *Handler) Update(c echo.Context) error {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
form, err := response.Parse[CMSForm](c)
|
form, err := ParseForm(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
return handleParseError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update CMS
|
|
||||||
cms, err := h.service.Update(c.Request().Context(), id, form)
|
cms, err := h.service.Update(c.Request().Context(), id, form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrCMSKeyExists {
|
return handleServiceError(c, err, "Failed to update marketing page")
|
||||||
return response.Conflict(c, "CMS with this key already exists")
|
|
||||||
}
|
|
||||||
if err.Error() == "cms not found" {
|
|
||||||
return response.NotFound(c, "CMS entry not found")
|
|
||||||
}
|
|
||||||
return response.InternalServerError(c, "Failed to update CMS entry")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, cms, "CMS entry updated successfully")
|
return response.Success(c, cms, "Marketing page updated successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete handles deleting a CMS entry (admin only)
|
// Delete handles deleting a CMS entry (admin only)
|
||||||
@@ -148,13 +134,10 @@ func (h *Handler) Delete(c echo.Context) error {
|
|||||||
|
|
||||||
err := h.service.Delete(c.Request().Context(), id)
|
err := h.service.Delete(c.Request().Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "cms not found" {
|
return handleServiceError(c, err, "Failed to delete marketing page")
|
||||||
return response.NotFound(c, "CMS entry not found")
|
|
||||||
}
|
|
||||||
return response.InternalServerError(c, "Failed to delete CMS entry")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, nil, "CMS entry deleted successfully")
|
return response.Success(c, nil, "Marketing page deleted successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search handles searching CMS entries (admin only)
|
// Search handles searching CMS entries (admin only)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package cms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tm/pkg/response"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleParseError(c echo.Context, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "invalid request format") {
|
||||||
|
return response.BadRequest(c, "Invalid request format", err.Error())
|
||||||
|
}
|
||||||
|
return response.ValidationError(c, "Please fix the validation errors", FormatValidationErrors(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceError(c echo.Context, err error, fallback string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrCMSKeyExists) {
|
||||||
|
return response.Conflict(c, "A marketing page with this key already exists")
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrCMSNotFound) {
|
||||||
|
return response.NotFound(c, "Marketing page not found")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, fallback)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package cms
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
@@ -90,6 +91,9 @@ func (s *cmsService) GetByID(ctx context.Context, id string) (*CMSResponse, erro
|
|||||||
|
|
||||||
cms, err := s.repo.GetByID(ctx, id)
|
cms, err := s.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrCMSNotFound) {
|
||||||
|
return nil, ErrCMSNotFound
|
||||||
|
}
|
||||||
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
|
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
|
||||||
"id": id,
|
"id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -108,6 +112,9 @@ func (s *cmsService) GetByKey(ctx context.Context, key string) (*CMSResponse, er
|
|||||||
|
|
||||||
cms, err := s.repo.GetByKey(ctx, key)
|
cms, err := s.repo.GetByKey(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrCMSNotFound) {
|
||||||
|
return nil, ErrCMSNotFound
|
||||||
|
}
|
||||||
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
|
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
|
||||||
"key": key,
|
"key": key,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -128,6 +135,9 @@ func (s *cmsService) Update(ctx context.Context, id string, form *CMSForm) (*CMS
|
|||||||
// Get existing CMS
|
// Get existing CMS
|
||||||
cms, err := s.repo.GetByID(ctx, id)
|
cms, err := s.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrCMSNotFound) {
|
||||||
|
return nil, ErrCMSNotFound
|
||||||
|
}
|
||||||
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
|
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
|
||||||
"id": id,
|
"id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -175,6 +185,9 @@ func (s *cmsService) Delete(ctx context.Context, id string) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := s.repo.Delete(ctx, id); err != nil {
|
if err := s.repo.Delete(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, ErrCMSNotFound) {
|
||||||
|
return ErrCMSNotFound
|
||||||
|
}
|
||||||
s.logger.Error("Failed to delete CMS", map[string]interface{}{
|
s.logger.Error("Failed to delete CMS", map[string]interface{}{
|
||||||
"id": id,
|
"id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package cms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
objectIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{24}$`)
|
||||||
|
filePathPattern = regexp.MustCompile(`^/[^/]*/v\d+/files/[a-fA-F0-9]{24}(/download)?$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationService registers CMS-specific govalidator rules.
|
||||||
|
type ValidationService struct{}
|
||||||
|
|
||||||
|
// NewValidationService registers custom validators used by CMS forms.
|
||||||
|
func NewValidationService() ValidationService {
|
||||||
|
govalidator.CustomTypeTagMap.Set("mediaRef", govalidator.CustomTypeValidator(func(i interface{}, _ interface{}) bool {
|
||||||
|
value, ok := i.(string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isMediaReference(value)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return ValidationService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMediaReference(value string) bool {
|
||||||
|
if value == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if govalidator.IsURL(value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if objectIDPattern.MatchString(value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return filePathPattern.MatchString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCMSForm validates a CMS create/update payload.
|
||||||
|
func ValidateCMSForm(form *CMSForm) error {
|
||||||
|
_, err := govalidator.ValidateStruct(form)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseForm binds and validates a CMS form from the request.
|
||||||
|
func ParseForm(c echo.Context) (*CMSForm, error) {
|
||||||
|
var form CMSForm
|
||||||
|
if err := c.Bind(&form); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid request format: %w", err)
|
||||||
|
}
|
||||||
|
if err := ValidateCMSForm(&form); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &form, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatValidationErrors converts govalidator errors into user-facing messages.
|
||||||
|
func FormatValidationErrors(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
errs, ok := err.(govalidator.Errors)
|
||||||
|
if !ok {
|
||||||
|
return humanizeValidationError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := make([]string, 0, len(errs))
|
||||||
|
for _, item := range errs {
|
||||||
|
messages = append(messages, humanizeValidationError(item.Error()))
|
||||||
|
}
|
||||||
|
return strings.Join(messages, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanizeValidationError(raw string) string {
|
||||||
|
field, rule, ok := parseGovalidatorError(raw)
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
label := fieldLabel(field)
|
||||||
|
lowerRule := strings.ToLower(rule)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(lowerRule, "mediaref"):
|
||||||
|
return label + " must be a valid URL or uploaded file"
|
||||||
|
case strings.Contains(lowerRule, "url"):
|
||||||
|
return label + " must be a valid URL"
|
||||||
|
case strings.Contains(lowerRule, "email"):
|
||||||
|
return label + " must be a valid email address"
|
||||||
|
case strings.Contains(lowerRule, "length"):
|
||||||
|
return label + " has an invalid length"
|
||||||
|
case strings.Contains(lowerRule, "range"):
|
||||||
|
return label + " is out of the allowed range"
|
||||||
|
case strings.Contains(lowerRule, "in("):
|
||||||
|
return label + " has an invalid value"
|
||||||
|
default:
|
||||||
|
return label + " is invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGovalidatorError(raw string) (field, rule string, ok bool) {
|
||||||
|
parts := strings.SplitN(raw, ": ", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return parts[0], parts[1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldLabel(field string) string {
|
||||||
|
field = strings.TrimPrefix(field, "CMSForm.")
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"Hero.", "Hero section: ",
|
||||||
|
"Chart.", "Chart section: ",
|
||||||
|
"Features.", "Features section: ",
|
||||||
|
"Challenges.", "Challenges section: ",
|
||||||
|
"Advantages.", "Advantages section: ",
|
||||||
|
"Contact.", "Contact section: ",
|
||||||
|
"Footer.", "Footer section: ",
|
||||||
|
".", " ",
|
||||||
|
)
|
||||||
|
label := replacer.Replace(field)
|
||||||
|
label = strings.ReplaceAll(label, "Cards 0", "card 1")
|
||||||
|
label = strings.ReplaceAll(label, "Cards 1", "card 2")
|
||||||
|
label = strings.ReplaceAll(label, "Cards 2", "card 3")
|
||||||
|
label = strings.ReplaceAll(label, "Cards 3", "card 4")
|
||||||
|
label = strings.ReplaceAll(label, "Cards 4", "card 5")
|
||||||
|
return label
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
|
|||||||
customer, err := h.service.Register(c.Request().Context(), form)
|
customer, err := h.service.Register(c.Request().Context(), form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "customer with this email already exists" ||
|
if err.Error() == "customer with this email already exists" ||
|
||||||
|
err.Error() == "customer with this username already exists" ||
|
||||||
err.Error() == "company with this name already exists" ||
|
err.Error() == "company with this name already exists" ||
|
||||||
err.Error() == "company with this registration number already exists" ||
|
err.Error() == "company with this registration number already exists" ||
|
||||||
err.Error() == "company with this tax ID already exists" {
|
err.Error() == "company with this tax ID already exists" {
|
||||||
@@ -126,6 +127,7 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
|
|||||||
return response.NotFound(c, "Customer not found")
|
return response.NotFound(c, "Customer not found")
|
||||||
}
|
}
|
||||||
if err.Error() == "customer with this email already exists" ||
|
if err.Error() == "customer with this email already exists" ||
|
||||||
|
err.Error() == "customer with this username already exists" ||
|
||||||
err.Error() == "company with this name already exists" ||
|
err.Error() == "company with this name already exists" ||
|
||||||
err.Error() == "company with this registration number already exists" ||
|
err.Error() == "company with this registration number already exists" ||
|
||||||
err.Error() == "company with this tax ID already exists" {
|
err.Error() == "company with this tax ID already exists" {
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
|||||||
return nil, errors.New("customer with this email already exists")
|
return nil, errors.New("customer with this email already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if username already exists
|
||||||
|
existingCustomer, _ = s.repository.GetByUsername(ctx, strings.ToLower(form.Username))
|
||||||
|
if existingCustomer != nil {
|
||||||
|
return nil, errors.New("customer with this username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
// Check validator
|
// Check validator
|
||||||
if !s.validator.IsValidUsername(form.Username) {
|
if !s.validator.IsValidUsername(form.Username) {
|
||||||
return nil, errors.New("invalid username format")
|
return nil, errors.New("invalid username format")
|
||||||
|
|||||||
@@ -87,3 +87,12 @@ type StatsResponse struct {
|
|||||||
TotalFeedback int64 `json:"total_feedback"`
|
TotalFeedback int64 `json:"total_feedback"`
|
||||||
LastUpdated int64 `json:"last_updated"`
|
LastUpdated int64 `json:"last_updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomerStatsResponse represents feedback statistics for a single customer.
|
||||||
|
type CustomerStatsResponse struct {
|
||||||
|
CustomerID string `json:"customer_id"`
|
||||||
|
TotalLikes int64 `json:"total_likes"`
|
||||||
|
TotalDislikes int64 `json:"total_dislikes"`
|
||||||
|
TotalFeedback int64 `json:"total_feedback"`
|
||||||
|
LastUpdated int64 `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ func (h *Handler) Toggle(c echo.Context) error {
|
|||||||
|
|
||||||
// ListFeedback retrieves feedback with pagination and filtering
|
// ListFeedback retrieves feedback with pagination and filtering
|
||||||
// @Summary List feedback with filters
|
// @Summary List feedback with filters
|
||||||
// @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company.
|
// @Description Retrieve paginated list of feedback for the authenticated customer with optional filters.
|
||||||
// @Tags Feedback
|
// @Tags Feedback
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -215,16 +215,16 @@ func (h *Handler) Toggle(c echo.Context) error {
|
|||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/feedback [get]
|
// @Router /api/v1/feedback [get]
|
||||||
func (h *Handler) PublicList(c echo.Context) error {
|
func (h *Handler) PublicList(c echo.Context) error {
|
||||||
companyID, err := user.GetCompanyIDFromContext(c)
|
customerID, err := customer.GetCustomerIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
|
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
|
||||||
}
|
}
|
||||||
|
|
||||||
form, err := response.Parse[SearchForm](c)
|
form, err := response.Parse[SearchForm](c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
}
|
}
|
||||||
form.Company = &companyID
|
form.Customer = &customerID
|
||||||
|
|
||||||
pagination, err := response.NewPagination(c)
|
pagination, err := response.NewPagination(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,12 +291,12 @@ func (h *Handler) PublicGetByTenderID(c echo.Context) error {
|
|||||||
return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing")
|
return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
companyID, err := user.GetCompanyIDFromContext(c)
|
customerID, err := customer.GetCustomerIDFromContext(c)
|
||||||
if err != nil || companyID == "" {
|
if err != nil {
|
||||||
return response.Unauthorized(c, "User must be associated with a company")
|
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
|
||||||
}
|
}
|
||||||
|
|
||||||
feedback, err := h.service.GetByTenderID(c.Request().Context(), tenderID, companyID)
|
feedback, err := h.service.GetByTenderIDForCustomer(c.Request().Context(), tenderID, customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrFeedbackNotFound) {
|
if errors.Is(err, ErrFeedbackNotFound) {
|
||||||
return response.NotFound(c, "Feedback not found")
|
return response.NotFound(c, "Feedback not found")
|
||||||
@@ -307,28 +307,44 @@ func (h *Handler) PublicGetByTenderID(c echo.Context) error {
|
|||||||
return response.Success(c, feedback, "Feedback retrieved successfully")
|
return response.Success(c, feedback, "Feedback retrieved successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompanyFeedbackStats retrieves feedback statistics for the authenticated user's company
|
// GetCustomerStats retrieves feedback statistics for the authenticated customer.
|
||||||
// @Summary Get company feedback statistics
|
// @Summary Get customer feedback statistics
|
||||||
// @Description Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations.
|
// @Description Retrieve feedback statistics (likes, dislikes, totals) for the authenticated customer only.
|
||||||
// @Tags Feedback
|
// @Tags Feedback
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} response.APIResponse{data=StatsResponse} "Company feedback statistics retrieved successfully"
|
// @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully"
|
||||||
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing company association"
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association"
|
||||||
|
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/feedback/stats/customer [get]
|
||||||
|
func (h *Handler) GetCustomerStats(c echo.Context) error {
|
||||||
|
customerID, err := customer.GetCustomerIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := h.service.CustomerStats(c.Request().Context(), customerID)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to retrieve customer stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, stats, "Customer stats retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompanyStats returns customer-scoped stats for backward compatibility with clients using /stats/company.
|
||||||
|
// @Summary Get feedback statistics (customer-scoped, legacy path)
|
||||||
|
// @Description Same as GET /feedback/stats/customer. Prefer /stats/customer for new integrations.
|
||||||
|
// @Tags Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association"
|
||||||
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
|
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
|
||||||
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/feedback/stats/company [get]
|
// @Router /api/v1/feedback/stats/company [get]
|
||||||
func (h *Handler) GetCompanyStats(c echo.Context) error {
|
func (h *Handler) GetCompanyStats(c echo.Context) error {
|
||||||
companyID, err := user.GetCompanyIDFromContext(c)
|
return h.GetCustomerStats(c)
|
||||||
if err != nil {
|
|
||||||
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
|
|
||||||
}
|
|
||||||
|
|
||||||
stats, err := h.service.CompanyStats(c.Request().Context(), companyID)
|
|
||||||
if err != nil {
|
|
||||||
return response.InternalServerError(c, "Failed to retrieve company stats")
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Success(c, stats, "Company stats retrieved successfully")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-35
@@ -20,9 +20,12 @@ type Repository interface {
|
|||||||
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Feedback], error)
|
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Feedback], error)
|
||||||
GetByID(ctx context.Context, id string) (*Feedback, error)
|
GetByID(ctx context.Context, id string) (*Feedback, error)
|
||||||
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
|
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
|
||||||
|
GetByCustomerID(ctx context.Context, customerID string, tenderID string) (*Feedback, error)
|
||||||
GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error)
|
GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error)
|
||||||
|
GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*Feedback, error)
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
|
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error)
|
||||||
|
CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM
|
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM
|
||||||
@@ -43,6 +46,11 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
|
|||||||
{Key: "tender_id", Value: 1},
|
{Key: "tender_id", Value: 1},
|
||||||
{Key: "company_id", Value: 1},
|
{Key: "company_id", Value: 1},
|
||||||
}),
|
}),
|
||||||
|
*orm.NewIndex("customer_id_idx", bson.D{{Key: "customer_id", Value: 1}}),
|
||||||
|
*orm.NewIndex("tender_customer_idx", bson.D{
|
||||||
|
{Key: "tender_id", Value: 1},
|
||||||
|
{Key: "customer_id", Value: 1},
|
||||||
|
}),
|
||||||
*orm.NewIndex("tender_type_idx", bson.D{
|
*orm.NewIndex("tender_type_idx", bson.D{
|
||||||
{Key: "tender_id", Value: 1},
|
{Key: "tender_id", Value: 1},
|
||||||
{Key: "feedback_type", Value: 1},
|
{Key: "feedback_type", Value: 1},
|
||||||
@@ -147,6 +155,23 @@ func (r *feedbackRepository) GetByCompanyID(ctx context.Context, companyID strin
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByCustomerID retrieves feedback by customer ID and tender ID.
|
||||||
|
func (r *feedbackRepository) GetByCustomerID(ctx context.Context, customerID string, tenderID string) (*Feedback, error) {
|
||||||
|
filter := bson.M{"customer_id": customerID, "tender_id": tenderID}
|
||||||
|
|
||||||
|
result, err := r.repo.FindOne(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback by customer ID and tender ID", map[string]interface{}{
|
||||||
|
"customer_id": customerID,
|
||||||
|
"tender_id": tenderID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetByTenderID retrieves feedback by tender ID and company ID
|
// GetByTenderID retrieves feedback by tender ID and company ID
|
||||||
func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error) {
|
func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error) {
|
||||||
filter := bson.M{"tender_id": tenderID, "company_id": companyID}
|
filter := bson.M{"tender_id": tenderID, "company_id": companyID}
|
||||||
@@ -167,6 +192,23 @@ func (r *feedbackRepository) GetByTenderID(ctx context.Context, tenderID, compan
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByTenderIDForCustomer retrieves feedback by tender ID and customer ID.
|
||||||
|
func (r *feedbackRepository) GetByTenderIDForCustomer(ctx context.Context, tenderID, customerID string) (*Feedback, error) {
|
||||||
|
filter := bson.M{"tender_id": tenderID, "customer_id": customerID}
|
||||||
|
|
||||||
|
result, err := r.repo.FindOne(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback by tender ID and customer ID", map[string]interface{}{
|
||||||
|
"tender_id": tenderID,
|
||||||
|
"customer_id": customerID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Delete deletes a feedback by ID
|
// Delete deletes a feedback by ID
|
||||||
func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
|
func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
|
||||||
err := r.repo.Delete(ctx, id)
|
err := r.repo.Delete(ctx, id)
|
||||||
@@ -209,20 +251,59 @@ func (r *feedbackRepository) Search(ctx context.Context, criteria SearchForm, pa
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats calculates feedback statistics for a company
|
// CompanyStats calculates feedback statistics for a company.
|
||||||
func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) {
|
func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string) (*StatsResponse, error) {
|
||||||
|
counts, lastUpdated, err := r.aggregateFeedbackCounts(ctx, bson.M{"company_id": companyID})
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback stats", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StatsResponse{
|
||||||
|
CompanyID: companyID,
|
||||||
|
TotalFeedback: counts.totalFeedback,
|
||||||
|
TotalLikes: counts.totalLikes,
|
||||||
|
TotalDislikes: counts.totalDislikes,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomerStats calculates feedback statistics for a customer.
|
||||||
|
func (r *feedbackRepository) CustomerStats(ctx context.Context, customerID string) (*CustomerStatsResponse, error) {
|
||||||
|
counts, lastUpdated, err := r.aggregateFeedbackCounts(ctx, bson.M{"customer_id": customerID})
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get customer feedback stats", map[string]interface{}{
|
||||||
|
"customer_id": customerID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CustomerStatsResponse{
|
||||||
|
CustomerID: customerID,
|
||||||
|
TotalFeedback: counts.totalFeedback,
|
||||||
|
TotalLikes: counts.totalLikes,
|
||||||
|
TotalDislikes: counts.totalDislikes,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedbackCountAggregate struct {
|
||||||
|
totalFeedback int64
|
||||||
|
totalLikes int64
|
||||||
|
totalDislikes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *feedbackRepository) aggregateFeedbackCounts(ctx context.Context, match bson.M) (feedbackCountAggregate, int64, error) {
|
||||||
pipeline := mongo.Pipeline{
|
pipeline := mongo.Pipeline{
|
||||||
{
|
{{Key: "$match", Value: match}},
|
||||||
{Key: "$match", Value: bson.M{
|
|
||||||
"company_id": companyID,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
{Key: "$group", Value: bson.M{
|
{Key: "$group", Value: bson.M{
|
||||||
"_id": nil,
|
"_id": nil,
|
||||||
"total_feedback": bson.M{
|
"total_feedback": bson.M{"$sum": 1},
|
||||||
"$sum": 1,
|
|
||||||
},
|
|
||||||
"total_likes": bson.M{
|
"total_likes": bson.M{
|
||||||
"$sum": bson.M{
|
"$sum": bson.M{
|
||||||
"$cond": []interface{}{
|
"$cond": []interface{}{
|
||||||
@@ -248,38 +329,37 @@ func (r *feedbackRepository) CompanyStats(ctx context.Context, companyID string)
|
|||||||
|
|
||||||
results, err := r.repo.Aggregate(ctx, pipeline)
|
results, err := r.repo.Aggregate(ctx, pipeline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to get feedback stats", map[string]interface{}{
|
return feedbackCountAggregate{}, time.Now().Unix(), err
|
||||||
"company_id": companyID,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := &StatsResponse{
|
counts := feedbackCountAggregate{}
|
||||||
CompanyID: companyID,
|
lastUpdated := time.Now().Unix()
|
||||||
TotalFeedback: 0,
|
if len(results) == 0 {
|
||||||
TotalLikes: 0,
|
return counts, lastUpdated, nil
|
||||||
TotalDislikes: 0,
|
|
||||||
LastUpdated: time.Now().Unix(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) > 0 {
|
result := results[0]
|
||||||
result := results[0]
|
counts.totalFeedback = aggregateInt64(result["total_feedback"])
|
||||||
if totalFeedback, ok := result["total_feedback"].(int32); ok {
|
counts.totalLikes = aggregateInt64(result["total_likes"])
|
||||||
stats.TotalFeedback = int64(totalFeedback)
|
counts.totalDislikes = aggregateInt64(result["total_dislikes"])
|
||||||
}
|
if v, ok := result["last_updated"].(int64); ok && v > 0 {
|
||||||
if totalLikes, ok := result["total_likes"].(int32); ok {
|
lastUpdated = v
|
||||||
stats.TotalLikes = int64(totalLikes)
|
|
||||||
}
|
|
||||||
if totalDislikes, ok := result["total_dislikes"].(int32); ok {
|
|
||||||
stats.TotalDislikes = int64(totalDislikes)
|
|
||||||
}
|
|
||||||
if lastUpdated, ok := result["last_updated"].(int64); ok {
|
|
||||||
stats.LastUpdated = lastUpdated
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats, nil
|
return counts, lastUpdated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregateInt64(value interface{}) int64 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int32:
|
||||||
|
return int64(v)
|
||||||
|
case int64:
|
||||||
|
return v
|
||||||
|
case float64:
|
||||||
|
return int64(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSearchFilter builds MongoDB filter from search criteria
|
// buildSearchFilter builds MongoDB filter from search criteria
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ type Service interface {
|
|||||||
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error)
|
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error)
|
||||||
Get(ctx context.Context, id string) (*FeedbackResponse, error)
|
Get(ctx context.Context, id string) (*FeedbackResponse, error)
|
||||||
GetByTenderID(ctx context.Context, tenderID, companyID 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
|
Delete(ctx context.Context, id string) error
|
||||||
CompanyStats(ctx context.Context, companyID string) (*StatsResponse, 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)
|
getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ func NewService(feedbackRepo Repository, tenderService tender.Service, companySe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) {
|
func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) {
|
||||||
existing, err := s.feedbackRepo.GetByCompanyID(ctx, *companyID, req.Tender)
|
existing, err := s.feedbackRepo.GetByCustomerID(ctx, *customerID, req.Tender)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
feedback := &Feedback{
|
feedback := &Feedback{
|
||||||
@@ -52,9 +54,10 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
|
|||||||
err = s.feedbackRepo.Create(ctx, feedback)
|
err = s.feedbackRepo.Create(ctx, feedback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to create new feedback", map[string]interface{}{
|
s.logger.Error("Failed to create new feedback", map[string]interface{}{
|
||||||
"tender_id": req.Tender,
|
"tender_id": req.Tender,
|
||||||
"company_id": *companyID,
|
"customer_id": *customerID,
|
||||||
"error": err.Error(),
|
"company_id": *companyID,
|
||||||
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -67,10 +70,10 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
|
|||||||
if existing.FeedbackType == req.Type {
|
if existing.FeedbackType == req.Type {
|
||||||
err = s.feedbackRepo.Delete(ctx, existing.GetID())
|
err = s.feedbackRepo.Delete(ctx, existing.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
|
s.logger.Error("Failed to delete existing feedback", map[string]interface{}{
|
||||||
"feedback_id": existing.GetID(),
|
"feedback_id": existing.GetID(),
|
||||||
"tender_id": req.Tender,
|
"tender_id": req.Tender,
|
||||||
"company_id": *companyID,
|
"customer_id": *customerID,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -98,7 +101,7 @@ func (s *feedbackService) Toggle(ctx context.Context, req ToggleFeedbackForm, cu
|
|||||||
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
|
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
|
||||||
"feedback_id": existing.GetID(),
|
"feedback_id": existing.GetID(),
|
||||||
"tender_id": req.Tender,
|
"tender_id": req.Tender,
|
||||||
"company_id": *companyID,
|
"customer_id": *customerID,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -181,6 +184,22 @@ func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID
|
|||||||
return feedback.ToResponse(tender, company), nil
|
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 {
|
func feedbackCompanyID(f Feedback) string {
|
||||||
if f.CompanyID == nil {
|
if f.CompanyID == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -350,3 +369,27 @@ func (s *feedbackService) CompanyStats(ctx context.Context, companyID string) (*
|
|||||||
|
|
||||||
return stats, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type SearchForm struct {
|
|||||||
Method string `query:"method" valid:"optional"`
|
Method string `query:"method" valid:"optional"`
|
||||||
EventType string `query:"event_type" valid:"optional"`
|
EventType string `query:"event_type" valid:"optional"`
|
||||||
Type string `query:"type" valid:"optional"`
|
Type string `query:"type" valid:"optional"`
|
||||||
Recipient string `query:"recipient" valid:"optional"`
|
Recipient []string `query:"recipient" json:"recipient" valid:"optional"`
|
||||||
Seen *bool `query:"seen" valid:"optional"`
|
Seen *bool `query:"seen" valid:"optional"`
|
||||||
Priority string `query:"priority" valid:"optional"`
|
Priority string `query:"priority" valid:"optional"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func (h *NotificationHandler) AdminNotifications(c echo.Context) error {
|
|||||||
return response.ValidationError(c, err.Error(), "")
|
return response.ValidationError(c, err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
form.Recipient = userID
|
form.Recipient = []string{userID}
|
||||||
form.Status = "sent"
|
form.Status = "sent"
|
||||||
|
|
||||||
pagination, err := response.NewPagination(c)
|
pagination, err := response.NewPagination(c)
|
||||||
@@ -261,7 +261,7 @@ func (h *NotificationHandler) CustomerNotifications(c echo.Context) error {
|
|||||||
return response.ValidationError(c, err.Error(), "")
|
return response.ValidationError(c, err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
form.Recipient = userID
|
form.Recipient = []string{userID}
|
||||||
form.Status = "sent"
|
form.Status = "sent"
|
||||||
|
|
||||||
pagination, err := response.NewPagination(c)
|
pagination, err := response.NewPagination(c)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ var (
|
|||||||
|
|
||||||
// Notification represents a stored notification in the database
|
// Notification represents a stored notification in the database
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID string `bson:"_id,omitempty" json:"id"`
|
orm.Model `bson:",inline"`
|
||||||
UserID string `bson:"user_id" json:"user_id"`
|
UserID string `bson:"user_id" json:"user_id"`
|
||||||
Title string `bson:"title" json:"title"`
|
Title string `bson:"title" json:"title"`
|
||||||
Message string `bson:"message" json:"message"`
|
Message string `bson:"message" json:"message"`
|
||||||
@@ -32,19 +32,27 @@ type Notification struct {
|
|||||||
Methods map[string]string `bson:"methods,omitempty" json:"methods,omitempty"`
|
Methods map[string]string `bson:"methods,omitempty" json:"methods,omitempty"`
|
||||||
Metadata map[string]any `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
Metadata map[string]any `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||||
ScheduleAt int64 `bson:"schedule_at,omitempty" json:"schedule_at,omitempty"`
|
ScheduleAt int64 `bson:"schedule_at,omitempty" json:"schedule_at,omitempty"`
|
||||||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
|
||||||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
|
||||||
Seen bool `bson:"seen" json:"seen"`
|
Seen bool `bson:"seen" json:"seen"`
|
||||||
SeenAt int64 `bson:"seen_at,omitempty" json:"seen_at,omitempty"`
|
SeenAt int64 `bson:"seen_at,omitempty" json:"seen_at,omitempty"`
|
||||||
IsScheduled bool `bson:"is_scheduled" json:"is_scheduled"`
|
IsScheduled bool `bson:"is_scheduled" json:"is_scheduled"`
|
||||||
ScheduledAt int64 `bson:"scheduled_at,omitempty" json:"scheduled_at,omitempty"`
|
ScheduledAt int64 `bson:"scheduled_at,omitempty" json:"scheduled_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetID sets the notification ID (implements IDSetter interface)
|
||||||
|
func (n *Notification) SetID(id string) {
|
||||||
|
n.Model.SetID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the notification ID (implements IDGetter interface)
|
||||||
|
func (n *Notification) GetID() string {
|
||||||
|
return n.Model.GetID()
|
||||||
|
}
|
||||||
|
|
||||||
// Repository defines methods for notification data access
|
// Repository defines methods for notification data access
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Create(ctx context.Context, notification *Notification) error
|
Create(ctx context.Context, notification *Notification) error
|
||||||
GetByID(ctx context.Context, id string) (*Notification, error)
|
GetByID(ctx context.Context, id string) (*Notification, error)
|
||||||
GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error)
|
GetByUserID(ctx context.Context, userIDs []string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error)
|
||||||
Update(ctx context.Context, notification *Notification) error
|
Update(ctx context.Context, notification *Notification) error
|
||||||
MarkAsSeen(ctx context.Context, notificationID, userID string) error
|
MarkAsSeen(ctx context.Context, notificationID, userID string) error
|
||||||
MarkAllAsSeen(ctx context.Context, userID string) error
|
MarkAllAsSeen(ctx context.Context, userID string) error
|
||||||
@@ -106,9 +114,15 @@ func (r *notificationRepository) GetByID(ctx context.Context, id string) (*Notif
|
|||||||
return r.ormRepo.FindByID(ctx, id)
|
return r.ormRepo.FindByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByUserID retrieves notifications for a specific user with filters
|
// GetByUserID retrieves notifications with optional user filters
|
||||||
func (r *notificationRepository) GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) {
|
func (r *notificationRepository) GetByUserID(ctx context.Context, userIDs []string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) {
|
||||||
filter := bson.M{"user_id": userID}
|
filter := bson.M{}
|
||||||
|
|
||||||
|
if len(userIDs) == 1 {
|
||||||
|
filter["user_id"] = userIDs[0]
|
||||||
|
} else if len(userIDs) > 1 {
|
||||||
|
filter["user_id"] = bson.M{"$in": userIDs}
|
||||||
|
}
|
||||||
|
|
||||||
if status != "" {
|
if status != "" {
|
||||||
filter["status"] = status
|
filter["status"] = status
|
||||||
@@ -146,8 +160,8 @@ func (r *notificationRepository) GetByUserID(ctx context.Context, userID string,
|
|||||||
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
|
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to find notifications", map[string]interface{}{
|
r.logger.Error("Failed to find notifications", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"user_id": userID,
|
"user_ids": userIDs,
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -173,8 +187,13 @@ func (r *notificationRepository) Update(ctx context.Context, notification *Notif
|
|||||||
|
|
||||||
// MarkAsSeen marks a notification as seen
|
// MarkAsSeen marks a notification as seen
|
||||||
func (r *notificationRepository) MarkAsSeen(ctx context.Context, notificationID, userID string) error {
|
func (r *notificationRepository) MarkAsSeen(ctx context.Context, notificationID, userID string) error {
|
||||||
|
objectID, err := bson.ObjectIDFromHex(notificationID)
|
||||||
|
if err != nil {
|
||||||
|
return ErrNotificationNotFound
|
||||||
|
}
|
||||||
|
|
||||||
filter := bson.M{
|
filter := bson.M{
|
||||||
"_id": notificationID,
|
"_id": objectID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,50 +76,7 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, recipient := range recipients {
|
for _, recipient := range recipients {
|
||||||
if slices.Contains(req.Channels, DeliveryChannelPush) {
|
s.sendToRecipient(ctx, recipient, req, link, image)
|
||||||
for _, v := range recipient.DeviceTokens {
|
|
||||||
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
||||||
UserID: recipient.UserID,
|
|
||||||
Title: req.Title,
|
|
||||||
Message: req.Description,
|
|
||||||
Type: string(req.Type),
|
|
||||||
Priority: string(req.Priority),
|
|
||||||
EventType: notification.EventTypePush,
|
|
||||||
Link: link,
|
|
||||||
Image: image,
|
|
||||||
Metadata: map[string]any{
|
|
||||||
"tender": strings.TrimSpace(req.Tender),
|
|
||||||
},
|
|
||||||
Methods: notification.NotificationMethods{
|
|
||||||
Push: v,
|
|
||||||
},
|
|
||||||
|
|
||||||
ScheduledAt: req.ScheduleAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(req.Channels, DeliveryChannelEmail) {
|
|
||||||
if recipient.Email != "" {
|
|
||||||
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
||||||
UserID: recipient.UserID,
|
|
||||||
Title: req.Title,
|
|
||||||
Message: req.Description,
|
|
||||||
Type: string(req.Type),
|
|
||||||
Priority: string(req.Priority),
|
|
||||||
EventType: notification.EventTypeEmail,
|
|
||||||
Link: link,
|
|
||||||
Image: image,
|
|
||||||
Metadata: map[string]any{
|
|
||||||
"tender": strings.TrimSpace(req.Tender),
|
|
||||||
},
|
|
||||||
Methods: notification.NotificationMethods{
|
|
||||||
Email: recipient.Email,
|
|
||||||
},
|
|
||||||
ScheduledAt: req.ScheduleAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target))
|
recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target))
|
||||||
@@ -127,60 +84,104 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, recipient := range recipients {
|
for _, recipient := range recipients {
|
||||||
if slices.Contains(req.Channels, DeliveryChannelPush) {
|
s.sendToRecipient(ctx, recipient, req, link, image)
|
||||||
for _, v := range recipient.DeviceTokens {
|
|
||||||
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
||||||
UserID: recipient.UserID,
|
|
||||||
Title: req.Title,
|
|
||||||
Message: req.Description,
|
|
||||||
Type: string(req.Type),
|
|
||||||
Priority: string(req.Priority),
|
|
||||||
EventType: notification.EventTypePush,
|
|
||||||
Link: link,
|
|
||||||
Image: image,
|
|
||||||
Methods: notification.NotificationMethods{
|
|
||||||
Push: v,
|
|
||||||
},
|
|
||||||
ScheduledAt: req.ScheduleAt,
|
|
||||||
Metadata: map[string]any{
|
|
||||||
"tender": strings.TrimSpace(req.Tender),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(req.Channels, DeliveryChannelEmail) {
|
|
||||||
if recipient.Email != "" {
|
|
||||||
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
|
||||||
UserID: recipient.UserID,
|
|
||||||
Title: req.Title,
|
|
||||||
Message: req.Description,
|
|
||||||
Type: string(req.Type),
|
|
||||||
Priority: string(req.Priority),
|
|
||||||
EventType: notification.EventTypeEmail,
|
|
||||||
Link: link,
|
|
||||||
Image: image,
|
|
||||||
Methods: notification.NotificationMethods{
|
|
||||||
Email: recipient.Email,
|
|
||||||
},
|
|
||||||
ScheduledAt: req.ScheduleAt,
|
|
||||||
Metadata: map[string]any{
|
|
||||||
"tender": strings.TrimSpace(req.Tender),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
s.logger.Info("Notification sent", map[string]interface{}{
|
|
||||||
"recipient": req.Recipient,
|
|
||||||
"channels": req.Channels,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Notification sent", map[string]interface{}{
|
||||||
|
"recipient": req.Recipient,
|
||||||
|
"channels": req.Channels,
|
||||||
|
"target": req.Target,
|
||||||
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *notificationService) sendToRecipient(ctx context.Context, recipient notificationRecipient, req *NotificationRequest, link, image string) {
|
||||||
|
if err := s.persistNotification(ctx, recipient, req, link, image); err != nil {
|
||||||
|
s.logger.Error("Failed to persist in-app notification", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"user_id": recipient.UserID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := map[string]any{
|
||||||
|
"tender": strings.TrimSpace(req.Tender),
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(req.Channels, DeliveryChannelPush) {
|
||||||
|
for _, v := range recipient.DeviceTokens {
|
||||||
|
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
||||||
|
UserID: recipient.UserID,
|
||||||
|
Title: req.Title,
|
||||||
|
Message: req.Description,
|
||||||
|
Type: string(req.Type),
|
||||||
|
Priority: string(req.Priority),
|
||||||
|
EventType: notification.EventTypePush,
|
||||||
|
Link: link,
|
||||||
|
Image: image,
|
||||||
|
Metadata: metadata,
|
||||||
|
Methods: notification.NotificationMethods{
|
||||||
|
Push: v,
|
||||||
|
},
|
||||||
|
ScheduledAt: req.ScheduleAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(req.Channels, DeliveryChannelEmail) && recipient.Email != "" {
|
||||||
|
s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{
|
||||||
|
UserID: recipient.UserID,
|
||||||
|
Title: req.Title,
|
||||||
|
Message: req.Description,
|
||||||
|
Type: string(req.Type),
|
||||||
|
Priority: string(req.Priority),
|
||||||
|
EventType: notification.EventTypeEmail,
|
||||||
|
Link: link,
|
||||||
|
Image: image,
|
||||||
|
Metadata: metadata,
|
||||||
|
Methods: notification.NotificationMethods{
|
||||||
|
Email: recipient.Email,
|
||||||
|
},
|
||||||
|
ScheduledAt: req.ScheduleAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notificationService) persistNotification(ctx context.Context, recipient notificationRecipient, req *NotificationRequest, link, image string) error {
|
||||||
|
methods := make(map[string]string)
|
||||||
|
if slices.Contains(req.Channels, DeliveryChannelEmail) && recipient.Email != "" {
|
||||||
|
methods["email"] = recipient.Email
|
||||||
|
}
|
||||||
|
if slices.Contains(req.Channels, DeliveryChannelPush) && len(recipient.DeviceTokens) > 0 {
|
||||||
|
methods["push"] = recipient.DeviceTokens[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
status := string(DeliveryStatusSent)
|
||||||
|
if req.ScheduleAt > 0 {
|
||||||
|
status = string(DeliveryStatusPending)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repository.Create(ctx, &Notification{
|
||||||
|
UserID: recipient.UserID,
|
||||||
|
Title: req.Title,
|
||||||
|
Message: req.Description,
|
||||||
|
Link: link,
|
||||||
|
Image: image,
|
||||||
|
Priority: string(req.Priority),
|
||||||
|
Type: string(req.Type),
|
||||||
|
Status: status,
|
||||||
|
Methods: methods,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"tender": strings.TrimSpace(req.Tender),
|
||||||
|
},
|
||||||
|
ScheduleAt: req.ScheduleAt,
|
||||||
|
ScheduledAt: req.ScheduleAt,
|
||||||
|
IsScheduled: req.ScheduleAt > 0,
|
||||||
|
Seen: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *notificationService) GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) {
|
func (s *notificationService) GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) {
|
||||||
s.logger.Info("Getting notifications", map[string]interface{}{
|
s.logger.Info("Getting notifications", map[string]interface{}{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
@@ -227,7 +228,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
|
|||||||
}
|
}
|
||||||
|
|
||||||
notificationsResponse = append(notificationsResponse, &NotificationResponse{
|
notificationsResponse = append(notificationsResponse, &NotificationResponse{
|
||||||
ID: notification.ID,
|
ID: notification.GetID(),
|
||||||
UserID: notification.UserID,
|
UserID: notification.UserID,
|
||||||
Recipient: recipient,
|
Recipient: recipient,
|
||||||
Title: notification.Title,
|
Title: notification.Title,
|
||||||
@@ -292,7 +293,7 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI
|
|||||||
}
|
}
|
||||||
|
|
||||||
detailedResponse := &NotificationResponse{
|
detailedResponse := &NotificationResponse{
|
||||||
ID: notification.ID,
|
ID: notification.GetID(),
|
||||||
UserID: notification.UserID,
|
UserID: notification.UserID,
|
||||||
Recipient: recipient,
|
Recipient: recipient,
|
||||||
Title: notification.Title,
|
Title: notification.Title,
|
||||||
@@ -378,11 +379,15 @@ func (s *notificationService) getUsers(ctx context.Context, userIDs []string) ([
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(recipients) == 0 {
|
if len(userIDs) > 0 || len(users.Users) == 0 {
|
||||||
return nil, errors.New("no users found")
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(recipients) == 0 {
|
||||||
|
return nil, errors.New("no users found")
|
||||||
|
}
|
||||||
|
|
||||||
return recipients, nil
|
return recipients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user