Add CMS Management Functionality

- Introduced a new CMS management feature, including the ability to create, retrieve, update, search, and delete CMS entries.
- Implemented the CMS entity, repository, service, and handler layers following Clean Architecture principles.
- Added API endpoints for CMS operations in Swagger documentation, ensuring comprehensive API specifications.
- Enhanced error handling and validation for CMS data, improving robustness and user experience.
- Updated the main application to initialize the CMS repository and service, integrating them into the existing system.
This commit is contained in:
n.nakhostin
2025-11-03 13:20:58 +03:30
parent 875447ac58
commit 9a444a1e7d
9 changed files with 1021 additions and 13 deletions
+21 -5
View File
@@ -53,6 +53,12 @@ package main
// @tag.name Admin-Notification
// @tag.description Administrative notification management operations for web panel including CRUD operations, bulk notifications, queue management, and comprehensive filtering with pagination
// @tag.name Admin-Contacts
// @tag.description Administrative contact management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
// @tag.name Admin-CMS
// @tag.description Administrative CMS management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
// @tag.name Authorization
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
@@ -77,12 +83,19 @@ package main
// @tag.name Notification
// @tag.description Public notification management operations for mobile application including notification listing and detailed notification information
// @tag.name Contacts
// @tag.description Public contact management operations for mobile application including contact submission and contact listing
// @tag.name CMS
// @tag.description Public CMS management operations for mobile application including CMS listing and detailed CMS information
import (
"context"
"fmt"
"net/http"
"time"
"tm/internal/assets"
"tm/internal/cms"
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/contact"
@@ -144,8 +157,9 @@ func main() {
tenderApprovalRepository := tender_approval.NewRepository(mongoManager, logger)
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
contactRepository := contact.NewContactRepository(mongoManager, logger)
cmsRepository := cms.NewRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact"},
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact", "cms"},
})
// Initialize validation services
@@ -164,8 +178,9 @@ func main() {
flagService := assets.NewService(logger, conf.Assets.FlagsPath)
notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
contactService := contact.NewService(contactRepository, logger)
cmsService := cms.NewService(cmsRepository, logger)
logger.Info("Services initialized successfully", map[string]interface{}{
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact"},
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms"},
})
// Initialize handlers with services
@@ -180,16 +195,17 @@ func main() {
flagHandler := assets.NewHandler(flagService, logger)
notificationHandler := notification.NewHandler(notificationService)
contactHandler := contact.NewHandler(contactService)
cmsHandler := cms.NewHandler(cmsService)
logger.Info("Handlers initialized successfully", map[string]interface{}{
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact"},
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms"},
})
// Initialize HTTP server
e := bootstrap.InitHTTPServer(conf, logger)
// Register routes
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler)
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler)
// Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
+38 -2
View File
@@ -2,8 +2,10 @@ package router
import (
"tm/internal/assets"
"tm/internal/cms"
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/contact"
"tm/internal/customer"
"tm/internal/feedback"
"tm/internal/inquiry"
@@ -15,7 +17,7 @@ import (
"github.com/labstack/echo/v4"
)
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler) {
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler) {
adminV1 := e.Group("/admin/v1")
// Admin Users Routes
@@ -141,9 +143,31 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
notificationsGP.GET("/mark-seen/:id", notificationHandler.MarkSeen)
notificationsGP.GET("/my", notificationHandler.AdminNotifications)
}
// Admin Contact Routes
contactsGP := adminV1.Group("/contacts")
{
contactsGP.Use(userHandler.AuthMiddleware())
contactsGP.GET("", contactHandler.Search)
contactsGP.GET("/stats", contactHandler.GetStats)
contactsGP.GET("/:id", contactHandler.GetByID)
contactsGP.PUT("/:id/status", contactHandler.UpdateStatus)
contactsGP.DELETE("/:id", contactHandler.Delete)
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler) {
// Admin CMS Routes
cmsGP := adminV1.Group("/cms")
{
// cmsGP.Use(userHandler.AuthMiddleware())
cmsGP.POST("", cmsHandler.Create)
cmsGP.GET("", cmsHandler.Search)
cmsGP.GET("/:id", cmsHandler.GetByID)
cmsGP.PUT("/:id", cmsHandler.Update)
cmsGP.DELETE("/:id", cmsHandler.Delete)
}
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler) {
v1 := e.Group("/api/v1")
customerGP := v1.Group("/profile")
@@ -221,4 +245,16 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
notificationsGP.GET("/mark", notificationHandler.PublicAllMarkSeen)
notificationsGP.GET("/mark/:id", notificationHandler.PublicMarkSeen)
}
// Public Contact Routes
contactsPublicGP := v1.Group("/contacts")
{
contactsPublicGP.POST("", contactHandler.Create)
}
// Public CMS Routes
cmsGP := v1.Group("/cms")
{
cmsGP.GET("/:key", cmsHandler.GetByKey)
}
}
+110
View File
@@ -0,0 +1,110 @@
package cms
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// CMS represents the content management system for the landing page
type CMS struct {
mongo.Model `bson:",inline"`
Key string `bson:"key" json:"key" validate:"required"`
Hero heroSection `bson:"hero" json:"hero"`
Chart chartSection `bson:"chart" json:"chart"`
Features cardSection `bson:"features" json:"features"`
Challenges cardSection `bson:"challenges" json:"challenges"`
Advantages cardSection `bson:"advantages" json:"advantages"`
Contact contactSection `bson:"contact" json:"contact"`
Footer footerSection `bson:"footer" json:"footer"`
}
// SetID sets the CMS ID (implements IDSetter interface)
func (c *CMS) SetID(id string) {
c.ID, _ = bson.ObjectIDFromHex(id)
}
// GetID returns the CMS ID (implements IDGetter interface)
func (c *CMS) GetID() string {
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (c *CMS) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (c *CMS) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *CMS) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *CMS) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// heroSection represents the hero section of the landing page
type heroSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
ButtonText string `bson:"button_text"`
ButtonLink string `bson:"button_link"`
GifFile string `bson:"gif_file"`
}
// chartSection represents the chart section of the landing page
type chartSection struct {
Title string `bson:"title" json:"title"`
MissedAmount string `bson:"missed_amount" json:"missedAmount"`
Data []chartPoint `bson:"data" json:"data"`
}
type chartPoint struct {
Key string `bson:"key" json:"key"`
Value int `bson:"value" json:"value"`
}
// cardSection represents the card section of the landing page
type cardSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
Cards []card `bson:"cards"`
}
// card represents a card in the card section
type card struct {
Icon string `bson:"icon"`
Title string `bson:"title"`
Description string `bson:"description"`
}
// contactSection represents the contact section of the landing page
type contactSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
SubmitButtonText string `bson:"submit_button_text"`
Fields []formField `bson:"fields"`
}
// formField represents a form field in the contact section
type formField struct {
Name string `bson:"name"`
Label string `bson:"label"`
Placeholder string `bson:"placeholder"`
Required bool `bson:"required"`
}
// footerSection represents the footer section of the landing page
type footerSection struct {
Email string `bson:"email"`
Phone string `bson:"phone"`
Location string `bson:"location"`
Tagline string `bson:"tagline"`
Copyright string `bson:"copyright"`
}
+13
View File
@@ -0,0 +1,13 @@
package cms
import "errors"
// Custom errors for cms domain
var (
ErrCMSNotFound = errors.New("cms not found")
ErrCMSKeyRequired = errors.New("cms key is required")
ErrCMSKeyExists = errors.New("cms with this key already exists")
ErrInvalidCMSData = errors.New("invalid cms data")
ErrUnauthorizedAccess = errors.New("unauthorized access to cms")
ErrInvalidKeyFormat = errors.New("invalid cms key format")
)
+120
View File
@@ -0,0 +1,120 @@
package cms
import "tm/pkg/response"
// CMSForm represents the form for creating/updating the CMS
type CMSForm struct {
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
Hero heroSectionForm `json:"hero" valid:"optional"`
Chart chartSectionForm `json:"chart" valid:"optional"`
Features cardSectionForm `json:"features" valid:"optional"`
Challenges cardSectionForm `json:"challenges" valid:"optional"`
Advantages cardSectionForm `json:"advantages" valid:"optional"`
Contact contactSectionForm `json:"contact" valid:"optional"`
Footer footerSectionForm `json:"footer" valid:"optional"`
}
// heroSectionForm represents the form for creating/updating the hero section
type heroSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Hero Title"`
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"`
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"`
}
// chartSectionForm represents the form for creating/updating the chart section
type chartSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Chart Title"`
MissedAmount string `json:"missed_amount" valid:"optional,length(2|100)" example:"Missed Amount"`
Data []chartPointForm `json:"data" valid:"optional"`
}
// chartPointForm represents the form for creating/updating a chart point
type chartPointForm struct {
Key string `json:"key" valid:"optional,length(2|100)" example:"Key"`
Value int `json:"value" valid:"optional,range(0|1000000)" example:"1000000"`
}
// cardSectionForm represents the form for creating/updating the card section
type cardSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
Cards []cardForm `json:"cards" valid:"optional"`
}
// cardForm represents the form for creating/updating a card
type cardForm struct {
Icon string `json:"icon" valid:"optional,url" example:"https://example.com/icon.png"`
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
}
// contactSectionForm represents the form for creating/updating the contact section
type contactSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Contact Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Contact Description"`
SubmitButtonText string `json:"submit_button_text" valid:"optional,length(2|100)" example:"Submit Button Text"`
Fields []formFieldForm `json:"fields" valid:"optional"`
}
// formFieldForm represents the form for creating/updating a form field
type formFieldForm struct {
Name string `json:"name" valid:"optional,length(2|100)" example:"Name"`
Label string `json:"label" valid:"optional,length(2|100)" example:"Label"`
Placeholder string `json:"placeholder" valid:"optional,length(2|100)" example:"Placeholder"`
Required bool `json:"required" valid:"optional" example:"true"`
}
// footerSectionForm represents the form for creating/updating the footer section
type footerSectionForm struct {
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
Phone string `json:"phone" valid:"optional,length(10|20)" example:"+1234567890"`
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
}
// SearchCMSForm represents the form for searching CMS entries
type SearchCMSForm struct {
Search *string `query:"q" valid:"optional"`
Key *string `query:"key" valid:"optional,length(2|100)"`
}
// CMSResponse represents the CMS data sent in API responses
type CMSResponse struct {
ID string `json:"id"`
Key string `json:"key"`
Hero heroSection `json:"hero"`
Chart chartSection `json:"chart"`
Features cardSection `json:"features"`
Challenges cardSection `json:"challenges"`
Advantages cardSection `json:"advantages"`
Contact contactSection `json:"contact"`
Footer footerSection `json:"footer"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// CMSListResponse represents the response for listing CMS entries
type CMSListResponse struct {
CMS []*CMSResponse `json:"cms"`
Meta *response.Meta `json:"-"`
}
// ToResponse converts CMS to CMSResponse
func (c *CMS) ToResponse() *CMSResponse {
return &CMSResponse{
ID: c.GetID(),
Key: c.Key,
Hero: c.Hero,
Chart: c.Chart,
Features: c.Features,
Challenges: c.Challenges,
Advantages: c.Advantages,
Contact: c.Contact,
Footer: c.Footer,
CreatedAt: c.GetCreatedAt(),
UpdatedAt: c.GetUpdatedAt(),
}
}
+190
View File
@@ -0,0 +1,190 @@
package cms
import (
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for CMS operations
type Handler struct {
service Service
}
// NewHandler creates a new CMS handler
func NewHandler(service Service) *Handler {
return &Handler{
service: service,
}
}
// GetByKey handles retrieving CMS content by key (public API)
// @Summary Get CMS content by key
// @Description Retrieve CMS content for public display by its key
// @Tags CMS
// @Accept json
// @Produce json
// @Param key path string true "CMS key"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/cms/{key} [get]
func (h *Handler) GetByKey(c echo.Context) error {
key := c.Param("key")
cms, err := h.service.GetByKey(c.Request().Context(), key)
if err != nil {
return response.NotFound(c, "CMS content not found")
}
return response.Success(c, cms, "CMS content retrieved successfully")
}
// Create handles creating a new CMS entry (admin only)
// @Summary Create a new CMS entry
// @Description Create a new CMS entry for content management (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param cms body CMSForm true "CMS information"
// @Success 201 {object} response.APIResponse{data=CMSResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Create CMS
cms, err := h.service.Create(c.Request().Context(), form)
if err != nil {
if err == ErrCMSKeyExists {
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")
}
// GetByID handles retrieving a CMS entry by ID (admin only)
// @Summary Get CMS by ID
// @Description Retrieve a CMS entry by its ID for editing (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [get]
func (h *Handler) GetByID(c echo.Context) error {
id := c.Param("id")
cms, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
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")
}
// Update handles updating a CMS entry (admin only)
// @Summary Update CMS entry
// @Description Update an existing CMS entry by its ID (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Param cms body CMSForm true "Updated CMS information"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [put]
func (h *Handler) Update(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[CMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Update CMS
cms, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err == ErrCMSKeyExists {
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")
}
// Delete handles deleting a CMS entry (admin only)
// @Summary Delete CMS entry
// @Description Delete a CMS entry by its ID (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Success 200 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [delete]
func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
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")
}
// Search handles searching CMS entries (admin only)
// @Summary Search CMS entries
// @Description Search CMS entries with filters and pagination (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param q query string false "Search query"
// @Param key query string false "CMS key filter"
// @Param limit query integer false "Number of CMS entries per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of CMS entries to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(key, created_at, updated_at) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CMSListResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchCMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
return response.InternalServerError(c, "Failed to search CMS entries")
}
return response.SuccessWithMeta(c, result, result.Meta, "CMS entries retrieved successfully")
}
+195
View File
@@ -0,0 +1,195 @@
package cms
import (
"context"
"errors"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Repository defines methods for CMS data access
type Repository interface {
Create(ctx context.Context, cms *CMS) error
Update(ctx context.Context, cms *CMS) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*CMS, error)
GetByKey(ctx context.Context, key string) (*CMS, error)
Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error)
}
// cmsRepository implements the Repository interface using the MongoDB ORM
type cmsRepository struct {
ormRepo orm.Repository[CMS]
logger logger.Logger
}
// NewRepository creates a new CMS repository
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []orm.Index{
*orm.CreateTextIndex("cms_search_idx", "key"),
*orm.NewIndex("key_idx", bson.D{{Key: "key", Value: 1}}),
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
}
// Create indexes
err := mongoManager.CreateIndexes("cms", indexes)
if err != nil {
logger.Warn("Failed to create CMS indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repository
ormRepo := orm.NewRepository[CMS](mongoManager.GetCollection("cms"), logger)
return &cmsRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new CMS entry
func (r *cmsRepository) Create(ctx context.Context, cms *CMS) error {
// Set created timestamp using Unix timestamp
now := time.Now().Unix()
cms.SetCreatedAt(now)
// Use ORM to create CMS
err := r.ormRepo.Create(ctx, cms)
if err != nil {
r.logger.Error("Failed to create CMS", map[string]interface{}{
"error": err.Error(),
"key": cms.Key,
})
return err
}
r.logger.Info("CMS created successfully", map[string]interface{}{
"id": cms.GetID(),
"key": cms.Key,
})
return nil
}
// Update updates a CMS entry
func (r *cmsRepository) Update(ctx context.Context, cms *CMS) error {
// Set updated timestamp using Unix timestamp
cms.SetUpdatedAt(time.Now().Unix())
// Use ORM to update CMS
err := r.ormRepo.Update(ctx, cms)
if err != nil {
r.logger.Error("Failed to update CMS", map[string]interface{}{
"error": err.Error(),
"id": cms.GetID(),
"key": cms.Key,
})
return err
}
r.logger.Info("CMS updated successfully", map[string]interface{}{
"id": cms.GetID(),
"key": cms.Key,
})
return nil
}
// GetByID retrieves a CMS entry by ID
func (r *cmsRepository) GetByID(ctx context.Context, id string) (*CMS, error) {
cms, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, ErrCMSNotFound
}
r.logger.Error("Failed to get CMS by ID", map[string]interface{}{
"error": err.Error(),
"id": id,
})
return nil, err
}
return cms, nil
}
// GetByKey retrieves a CMS entry by key
func (r *cmsRepository) GetByKey(ctx context.Context, key string) (*CMS, error) {
cms, err := r.ormRepo.FindOne(ctx, bson.M{"key": key})
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, ErrCMSNotFound
}
r.logger.Error("Failed to get CMS by key", map[string]interface{}{
"error": err.Error(),
"key": key,
})
return nil, err
}
return cms, nil
}
// Delete deletes a CMS entry
func (r *cmsRepository) Delete(ctx context.Context, id string) error {
// Get CMS first to log the key
cms, err := r.GetByID(ctx, id)
if err != nil {
r.logger.Error("Failed to delete CMS", map[string]interface{}{
"error": err.Error(),
"id": id,
})
if errors.Is(err, ErrCMSNotFound) {
return ErrCMSNotFound
}
return err
}
// Delete from database
err = r.ormRepo.Delete(ctx, id)
if err != nil {
r.logger.Error("Failed to delete CMS", map[string]interface{}{
"error": err.Error(),
"id": id,
"key": cms.Key,
})
return err
}
r.logger.Info("CMS deleted successfully", map[string]interface{}{
"id": id,
"key": cms.Key,
})
return nil
}
// Search searches CMS entries with filters and pagination
func (r *cmsRepository) Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error) {
filter := bson.M{}
if search.Search != nil && *search.Search != "" {
filter["$or"] = bson.A{
bson.M{"key": bson.M{"$regex": search.Search, "$options": "i"}},
}
}
if search.Key != nil && *search.Key != "" {
filter["key"] = bson.M{"$regex": search.Key, "$options": "i"}
}
// Use ORM to find CMS entries
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to search CMS entries", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result, nil
}
+328
View File
@@ -0,0 +1,328 @@
package cms
import (
"context"
"fmt"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
)
// Service defines the interface for CMS business operations
type Service interface {
// Create creates a new CMS entry
Create(ctx context.Context, form *CMSForm) (*CMSResponse, error)
// GetByID retrieves a CMS entry by ID
GetByID(ctx context.Context, id string) (*CMSResponse, error)
// GetByKey retrieves a CMS entry by key
GetByKey(ctx context.Context, key string) (*CMSResponse, error)
// Update updates a CMS entry
Update(ctx context.Context, id string, form *CMSForm) (*CMSResponse, error)
// Delete deletes a CMS entry by ID
Delete(ctx context.Context, id string) error
// Search searches CMS entries with filters and pagination
Search(ctx context.Context, form *SearchCMSForm, pagination *response.Pagination) (*CMSListResponse, error)
}
// cmsService implements Service interface
type cmsService struct {
repo Repository
logger logger.Logger
}
// NewService creates a new CMS service
func NewService(repo Repository, logger logger.Logger) Service {
return &cmsService{
repo: repo,
logger: logger,
}
}
// Create creates a new CMS entry
func (s *cmsService) Create(ctx context.Context, form *CMSForm) (*CMSResponse, error) {
s.logger.Info("Creating new CMS entry", map[string]interface{}{
"key": form.Key,
})
// Check if CMS with this key already exists
existing, err := s.repo.GetByKey(ctx, form.Key)
if err == nil && existing != nil {
s.logger.Warn("CMS with this key already exists", map[string]interface{}{
"key": form.Key,
})
return nil, ErrCMSKeyExists
}
// Create CMS entity
cms := &CMS{
Key: form.Key,
}
// Copy form data to entity
s.copyFormToEntity(form, cms)
// Save to database
if err := s.repo.Create(ctx, cms); err != nil {
s.logger.Error("Failed to create CMS", map[string]interface{}{
"key": form.Key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to create CMS: %w", err)
}
s.logger.Info("CMS entry created successfully", map[string]interface{}{
"id": cms.GetID(),
"key": form.Key,
})
return cms.ToResponse(), nil
}
// GetByID retrieves a CMS entry by ID
func (s *cmsService) GetByID(ctx context.Context, id string) (*CMSResponse, error) {
s.logger.Debug("Retrieving CMS by ID", map[string]interface{}{
"id": id,
})
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS: %w", err)
}
return cms.ToResponse(), nil
}
// GetByKey retrieves a CMS entry by key
func (s *cmsService) GetByKey(ctx context.Context, key string) (*CMSResponse, error) {
s.logger.Debug("Retrieving CMS by key", map[string]interface{}{
"key": key,
})
cms, err := s.repo.GetByKey(ctx, key)
if err != nil {
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
"key": key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS: %w", err)
}
return cms.ToResponse(), nil
}
// Update updates a CMS entry
func (s *cmsService) Update(ctx context.Context, id string, form *CMSForm) (*CMSResponse, error) {
s.logger.Info("Updating CMS entry", map[string]interface{}{
"id": id,
"key": form.Key,
})
// Get existing CMS
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS for update: %w", err)
}
// Check if key is being changed and if new key already exists
if cms.Key != form.Key {
existing, err := s.repo.GetByKey(ctx, form.Key)
if err == nil && existing != nil && existing.GetID() != id {
s.logger.Warn("CMS with this key already exists", map[string]interface{}{
"key": form.Key,
})
return nil, ErrCMSKeyExists
}
cms.Key = form.Key
}
// Copy form data to entity
s.copyFormToEntity(form, cms)
// Update in database
if err := s.repo.Update(ctx, cms); err != nil {
s.logger.Error("Failed to update CMS", map[string]interface{}{
"id": id,
"key": form.Key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to update CMS: %w", err)
}
s.logger.Info("CMS entry updated successfully", map[string]interface{}{
"id": id,
"key": form.Key,
})
return cms.ToResponse(), nil
}
// Delete deletes a CMS entry by ID
func (s *cmsService) Delete(ctx context.Context, id string) error {
s.logger.Info("Deleting CMS entry", map[string]interface{}{
"id": id,
})
if err := s.repo.Delete(ctx, id); err != nil {
s.logger.Error("Failed to delete CMS", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to delete CMS: %w", err)
}
s.logger.Info("CMS entry deleted successfully", map[string]interface{}{
"id": id,
})
return nil
}
// Search searches CMS entries with filters and pagination
func (s *cmsService) Search(ctx context.Context, form *SearchCMSForm, pagination *response.Pagination) (*CMSListResponse, error) {
s.logger.Debug("Searching CMS entries", map[string]interface{}{
"filters": form,
"pagination": pagination,
})
// Convert response.Pagination to orm.Pagination
ormPagination := orm.Pagination{
Limit: pagination.Limit,
Skip: pagination.Offset,
SortField: pagination.SortBy,
SortOrder: 1, // Default ascending
}
if pagination.SortOrder == "desc" {
ormPagination.SortOrder = -1
}
result, err := s.repo.Search(ctx, form, ormPagination)
if err != nil {
s.logger.Error("Failed to search CMS entries", map[string]interface{}{
"filters": form,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to search CMS entries: %w", err)
}
// Convert to response format
cmsResponses := make([]*CMSResponse, len(result.Items))
for i, cms := range result.Items {
cmsResponses[i] = cms.ToResponse()
}
s.logger.Info("CMS search completed", map[string]interface{}{
"total_count": result.TotalCount,
"items_count": len(result.Items),
})
return &CMSListResponse{
CMS: cmsResponses,
Meta: pagination.Response(result.TotalCount),
}, nil
}
// copyFormToEntity copies data from form to entity
func (s *cmsService) copyFormToEntity(form *CMSForm, cms *CMS) {
// Hero section
if form.Hero.Title != "" {
cms.Hero.Title = form.Hero.Title
}
if form.Hero.Description != "" {
cms.Hero.Description = form.Hero.Description
}
if form.Hero.ButtonText != "" {
cms.Hero.ButtonText = form.Hero.ButtonText
}
if form.Hero.ButtonLink != "" {
cms.Hero.ButtonLink = form.Hero.ButtonLink
}
if form.Hero.GifFile != "" {
cms.Hero.GifFile = form.Hero.GifFile
}
// Chart section
if form.Chart.Title != "" {
cms.Chart.Title = form.Chart.Title
}
if form.Chart.MissedAmount != "" {
cms.Chart.MissedAmount = form.Chart.MissedAmount
}
if len(form.Chart.Data) > 0 {
cms.Chart.Data = make([]chartPoint, len(form.Chart.Data))
for i, point := range form.Chart.Data {
cms.Chart.Data[i] = chartPoint(point)
}
}
// Features section
s.copyCardSectionForm(&form.Features, &cms.Features)
// Challenges section
s.copyCardSectionForm(&form.Challenges, &cms.Challenges)
// Advantages section
s.copyCardSectionForm(&form.Advantages, &cms.Advantages)
// Contact section
if form.Contact.Title != "" {
cms.Contact.Title = form.Contact.Title
}
if form.Contact.Description != "" {
cms.Contact.Description = form.Contact.Description
}
if form.Contact.SubmitButtonText != "" {
cms.Contact.SubmitButtonText = form.Contact.SubmitButtonText
}
if len(form.Contact.Fields) > 0 {
cms.Contact.Fields = make([]formField, len(form.Contact.Fields))
for i, field := range form.Contact.Fields {
cms.Contact.Fields[i] = formField(field)
}
}
// Footer section
if form.Footer.Email != "" {
cms.Footer.Email = form.Footer.Email
}
if form.Footer.Phone != "" {
cms.Footer.Phone = form.Footer.Phone
}
if form.Footer.Location != "" {
cms.Footer.Location = form.Footer.Location
}
if form.Footer.Tagline != "" {
cms.Footer.Tagline = form.Footer.Tagline
}
if form.Footer.Copyright != "" {
cms.Footer.Copyright = form.Footer.Copyright
}
}
// copyCardSectionForm copies card section form data to entity
func (s *cmsService) copyCardSectionForm(formSection *cardSectionForm, entitySection *cardSection) {
if formSection.Title != "" {
entitySection.Title = formSection.Title
}
if formSection.Description != "" {
entitySection.Description = formSection.Description
}
if len(formSection.Cards) > 0 {
entitySection.Cards = make([]card, len(formSection.Cards))
for i, cardForm := range formSection.Cards {
entitySection.Cards[i] = card(cardForm)
}
}
}
+6 -6
View File
@@ -21,7 +21,7 @@ func NewHandler(service Service) *Handler {
// Create handles creating a new contact message
// @Summary Create a new contact message
// @Description Create a new contact message from a user
// @Tags contacts
// @Tags Contacts
// @Accept json
// @Produce json
// @Param contact body CreateContactForm true "Contact information"
@@ -52,7 +52,7 @@ func (h *Handler) Create(c echo.Context) error {
// GetByID handles retrieving a contact by ID (admin only)
// @Summary Get contact by ID
// @Description Retrieve a contact message by its ID (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
@@ -78,7 +78,7 @@ func (h *Handler) GetByID(c echo.Context) error {
// Search handles searching contacts with filters (admin only)
// @Summary Search contacts
// @Description Search contacts with filters and pagination (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param q query string false "Search query"
@@ -112,7 +112,7 @@ func (h *Handler) Search(c echo.Context) error {
// UpdateStatus handles updating contact status (admin only)
// @Summary Update contact status
// @Description Update the status of a contact message (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
@@ -152,7 +152,7 @@ func (h *Handler) UpdateStatus(c echo.Context) error {
// GetStats handles retrieving contact statistics (admin only)
// @Summary Get contact statistics
// @Description Get contact statistics by status (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=ContactStats}
@@ -170,7 +170,7 @@ func (h *Handler) GetStats(c echo.Context) error {
// Delete handles deleting a contact (admin only)
// @Summary Delete contact
// @Description Delete a contact message by ID (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"