package service import ( "encoding/json" "errors" "strconv" "strings" "time" "web-qudo-be/app/database" "web-qudo-be/app/database/entity" "web-qudo-be/app/database/entity/users" aboutUsImageSvc "web-qudo-be/app/module/about_us_content_images/service" aboutUsSvc "web-qudo-be/app/module/about_us_contents/service" "web-qudo-be/app/module/cms_content_submissions/repository" "web-qudo-be/app/module/cms_content_submissions/request" "web-qudo-be/app/module/cms_content_submissions/response" heroImageSvc "web-qudo-be/app/module/hero_content_images/service" heroSvc "web-qudo-be/app/module/hero_contents/service" ourProductImageSvc "web-qudo-be/app/module/our_product_content_images/service" ourProductSvc "web-qudo-be/app/module/our_product_contents/service" ourServiceImageSvc "web-qudo-be/app/module/our_service_content_images/service" ourServiceSvc "web-qudo-be/app/module/our_service_contents/service" partnerSvc "web-qudo-be/app/module/partner_contents/service" popupImageReq "web-qudo-be/app/module/popup_news_content_images/request" popupImageSvc "web-qudo-be/app/module/popup_news_content_images/service" popupNewsReq "web-qudo-be/app/module/popup_news_contents/request" popupSvc "web-qudo-be/app/module/popup_news_contents/service" "web-qudo-be/utils/paginator" "github.com/google/uuid" "github.com/rs/zerolog" ) const ( cmsSubmissionPending = "pending" cmsSubmissionApproved = "approved" cmsSubmissionRejected = "rejected" userLevelContributor = uint(2) userLevelApprover = uint(3) ) type CmsContentSubmissionsService interface { Submit(clientID *uuid.UUID, user *users.Users, req *request.SubmitCmsContentSubmissionRequest) (*entity.CmsContentSubmission, error) List(clientID *uuid.UUID, user *users.Users, status string, mineOnly bool, p *paginator.Pagination) ([]response.CmsContentSubmissionListItem, *paginator.Pagination, error) Approve(clientID *uuid.UUID, user *users.Users, id uuid.UUID) error Reject(clientID *uuid.UUID, user *users.Users, id uuid.UUID, note string) error } type cmsContentSubmissionsService struct { Repo repository.CmsContentSubmissionsRepository DB *database.Database Hero heroSvc.HeroContentsService HeroImg heroImageSvc.HeroContentImagesService About aboutUsSvc.AboutUsContentService AboutImg aboutUsImageSvc.AboutUsContentImageService OurProduct ourProductSvc.OurProductContentService OurProductImg ourProductImageSvc.OurProductContentImagesService OurService ourServiceSvc.OurServiceContentService OurServiceImg ourServiceImageSvc.OurServiceContentImagesService Partner partnerSvc.PartnerContentService Popup popupSvc.PopupNewsContentsService PopupImg popupImageSvc.PopupNewsContentImagesService Log zerolog.Logger } func NewCmsContentSubmissionsService( repo repository.CmsContentSubmissionsRepository, db *database.Database, hero heroSvc.HeroContentsService, heroImg heroImageSvc.HeroContentImagesService, about aboutUsSvc.AboutUsContentService, aboutImg aboutUsImageSvc.AboutUsContentImageService, ourProduct ourProductSvc.OurProductContentService, ourProductImg ourProductImageSvc.OurProductContentImagesService, ourService ourServiceSvc.OurServiceContentService, ourServiceImg ourServiceImageSvc.OurServiceContentImagesService, partner partnerSvc.PartnerContentService, popup popupSvc.PopupNewsContentsService, popupImg popupImageSvc.PopupNewsContentImagesService, log zerolog.Logger, ) CmsContentSubmissionsService { return &cmsContentSubmissionsService{ Repo: repo, DB: db, Hero: hero, HeroImg: heroImg, About: about, AboutImg: aboutImg, OurProduct: ourProduct, OurProductImg: ourProductImg, OurService: ourService, OurServiceImg: ourServiceImg, Partner: partner, Popup: popup, PopupImg: popupImg, Log: log, } } func (_i *cmsContentSubmissionsService) Submit(clientID *uuid.UUID, user *users.Users, req *request.SubmitCmsContentSubmissionRequest) (*entity.CmsContentSubmission, error) { if clientID == nil || user == nil { return nil, errors.New("unauthorized") } if user.UserLevelId != userLevelContributor { return nil, errors.New("only contributor (user level 2) can submit CMS drafts") } domain := strings.TrimSpace(strings.ToLower(req.Domain)) if domain == "" { return nil, errors.New("domain is required") } title := strings.TrimSpace(req.Title) if title == "" { return nil, errors.New("title is required") } if len(req.Payload) == 0 || string(req.Payload) == "null" { return nil, errors.New("payload is required") } row := &entity.CmsContentSubmission{ ID: uuid.New(), ClientID: *clientID, Domain: domain, Title: title, Status: cmsSubmissionPending, Payload: string(req.Payload), SubmittedByID: user.ID, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := _i.Repo.Create(row); err != nil { return nil, err } return row, nil } func (_i *cmsContentSubmissionsService) List(clientID *uuid.UUID, user *users.Users, status string, mineOnly bool, p *paginator.Pagination) ([]response.CmsContentSubmissionListItem, *paginator.Pagination, error) { if clientID == nil || user == nil { return nil, p, errors.New("unauthorized") } st := strings.TrimSpace(strings.ToLower(status)) var submittedBy *uint if mineOnly { submittedBy = &user.ID } else if user.UserLevelId == userLevelContributor { submittedBy = &user.ID } statusArg := status if st == "" { statusArg = "all" } rows, paging, err := _i.Repo.List(*clientID, statusArg, submittedBy, p) if err != nil { return nil, paging, err } out := make([]response.CmsContentSubmissionListItem, 0, len(rows)) for _, row := range rows { name := "" var u users.Users if err := _i.DB.DB.Select("fullname").Where("id = ?", row.SubmittedByID).First(&u).Error; err == nil { name = u.Fullname } out = append(out, response.CmsContentSubmissionListItem{ ID: row.ID, Domain: row.Domain, Title: row.Title, Status: row.Status, Payload: row.Payload, SubmittedByID: row.SubmittedByID, SubmitterName: name, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, }) } return out, paging, nil } func (_i *cmsContentSubmissionsService) Approve(clientID *uuid.UUID, user *users.Users, id uuid.UUID) error { if clientID == nil || user == nil { return errors.New("unauthorized") } if user.UserLevelId != userLevelApprover { return errors.New("only approver (user level 3) can approve CMS submissions") } row, err := _i.Repo.FindByID(*clientID, id) if err != nil { return err } if row.Status != cmsSubmissionPending { return errors.New("submission is not pending") } if err := _i.applyDomainPayload(row.Domain, row.Payload); err != nil { return err } now := time.Now() row.Status = cmsSubmissionApproved row.ReviewedByID = &user.ID row.ReviewNote = "" row.UpdatedAt = now return _i.Repo.Update(row) } func (_i *cmsContentSubmissionsService) Reject(clientID *uuid.UUID, user *users.Users, id uuid.UUID, note string) error { if clientID == nil || user == nil { return errors.New("unauthorized") } if user.UserLevelId != userLevelApprover { return errors.New("only approver (user level 3) can reject CMS submissions") } row, err := _i.Repo.FindByID(*clientID, id) if err != nil { return err } if row.Status != cmsSubmissionPending { return errors.New("submission is not pending") } now := time.Now() row.Status = cmsSubmissionRejected row.ReviewedByID = &user.ID row.ReviewNote = strings.TrimSpace(note) row.UpdatedAt = now return _i.Repo.Update(row) } func (_i *cmsContentSubmissionsService) applyDomainPayload(domain string, payloadJSON string) error { switch strings.ToLower(strings.TrimSpace(domain)) { case "hero": return _i.mergeHero([]byte(payloadJSON)) case "about": return _i.mergeAbout([]byte(payloadJSON)) case "product": return _i.mergeProduct([]byte(payloadJSON)) case "service": return _i.mergeService([]byte(payloadJSON)) case "partner": return _i.mergePartner([]byte(payloadJSON)) case "popup": return _i.mergePopup([]byte(payloadJSON)) default: return errors.New("unknown domain") } } type heroPayload struct { Action string `json:"action"` HeroID string `json:"hero_id"` HeroImageID string `json:"hero_image_id"` PrimaryTitle string `json:"primary_title"` SecondaryTitle string `json:"secondary_title"` Description string `json:"description"` PrimaryCta string `json:"primary_cta"` SecondaryCtaText string `json:"secondary_cta_text"` ImageURL string `json:"image_url"` } func (_i *cmsContentSubmissionsService) mergeHero(raw []byte) error { var p heroPayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { return errors.New("hero delete is not supported") } ent := &entity.HeroContents{ PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, PrimaryCta: p.PrimaryCta, SecondaryCtaText: p.SecondaryCtaText, } var heroUUID uuid.UUID if strings.TrimSpace(p.HeroID) == "" { saved, err := _i.Hero.Save(ent) if err != nil { return err } heroUUID = saved.ID } else { id, err := uuid.Parse(p.HeroID) if err != nil { return err } heroUUID = id if err := _i.Hero.Update(heroUUID, ent); err != nil { return err } } imgURL := strings.TrimSpace(p.ImageURL) if imgURL == "" { return nil } if strings.TrimSpace(p.HeroImageID) != "" { imgID, err := uuid.Parse(p.HeroImageID) if err != nil { return err } return _i.HeroImg.Update(imgID, &entity.HeroContentImages{ ID: imgID, HeroContentID: heroUUID, ImageURL: imgURL, }) } _, err := _i.HeroImg.Save(&entity.HeroContentImages{ HeroContentID: heroUUID, ImageURL: imgURL, }) return err } type aboutPayload struct { Action string `json:"action"` AboutID *int `json:"about_id"` AboutMediaImageID *int `json:"about_media_image_id"` PrimaryTitle string `json:"primary_title"` SecondaryTitle string `json:"secondary_title"` Description string `json:"description"` PrimaryCta string `json:"primary_cta"` SecondaryCtaText string `json:"secondary_cta_text"` MediaURL string `json:"media_url"` } func (_i *cmsContentSubmissionsService) mergeAbout(raw []byte) error { var p aboutPayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { return errors.New("about delete is not supported") } ent := &entity.AboutUsContent{ PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, PrimaryCta: p.PrimaryCta, SecondaryCtaText: p.SecondaryCtaText, } var aboutID uint if p.AboutID == nil || *p.AboutID == 0 { saved, err := _i.About.Save(ent) if err != nil { return err } aboutID = saved.ID } else { aboutID = uint(*p.AboutID) if err := _i.About.Update(aboutID, ent); err != nil { return err } } mediaURL := strings.TrimSpace(p.MediaURL) if mediaURL == "" { return nil } if p.AboutMediaImageID != nil && *p.AboutMediaImageID > 0 { _ = _i.AboutImg.Delete(uint(*p.AboutMediaImageID)) } _, err := _i.AboutImg.SaveRemoteURL(aboutID, mediaURL, "") return err } type productPayload struct { Action string `json:"action"` ProductID string `json:"product_id"` ProductImageID string `json:"product_image_id"` PrimaryTitle string `json:"primary_title"` SecondaryTitle string `json:"secondary_title"` Description string `json:"description"` LinkURL string `json:"link_url"` ImageURL string `json:"image_url"` } func (_i *cmsContentSubmissionsService) mergeProduct(raw []byte) error { var p productPayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { if strings.TrimSpace(p.ProductID) == "" { return errors.New("product_id required for delete") } id, err := uuid.Parse(p.ProductID) if err != nil { return err } return _i.OurProduct.Delete(id) } ent := &entity.OurProductContent{ PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, LinkURL: p.LinkURL, } var pid uuid.UUID if strings.TrimSpace(p.ProductID) == "" { saved, err := _i.OurProduct.Save(ent) if err != nil { return err } pid = saved.ID } else { id, err := uuid.Parse(p.ProductID) if err != nil { return err } pid = id if err := _i.OurProduct.Update(pid, ent); err != nil { return err } } imgURL := strings.TrimSpace(p.ImageURL) if imgURL == "" { return nil } if strings.TrimSpace(p.ProductImageID) != "" { imgID, err := uuid.Parse(p.ProductImageID) if err != nil { return err } return _i.OurProductImg.Update(imgID, &entity.OurProductContentImage{ ID: imgID, OurProductContentID: pid, ImageURL: imgURL, }) } _, err := _i.OurProductImg.Save(&entity.OurProductContentImage{ OurProductContentID: pid, ImageURL: imgURL, }) return err } type servicePayload struct { Action string `json:"action"` ServiceID *int `json:"service_id"` ServiceImageID string `json:"service_image_id"` PrimaryTitle string `json:"primary_title"` SecondaryTitle string `json:"secondary_title"` Description string `json:"description"` LinkURL string `json:"link_url"` ImageURL string `json:"image_url"` } func (_i *cmsContentSubmissionsService) mergeService(raw []byte) error { var p servicePayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { if p.ServiceID == nil || *p.ServiceID == 0 { return errors.New("service_id required for delete") } return _i.OurService.Delete(uint(*p.ServiceID)) } ent := &entity.OurServiceContent{ PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, LinkURL: p.LinkURL, } var sid uint if p.ServiceID == nil || *p.ServiceID == 0 { saved, err := _i.OurService.Save(ent) if err != nil { return err } sid = saved.ID } else { sid = uint(*p.ServiceID) if err := _i.OurService.Update(sid, ent); err != nil { return err } } imgURL := strings.TrimSpace(p.ImageURL) if imgURL == "" { return nil } return _i.mergeServiceImage(sid, strings.TrimSpace(p.ServiceImageID), imgURL) } func (_i *cmsContentSubmissionsService) mergeServiceImage(sid uint, serviceImageID string, imgURL string) error { if serviceImageID != "" { n, err := strconv.ParseUint(serviceImageID, 10, 64) if err != nil { return err } imgID := uint(n) return _i.OurServiceImg.Update(imgID, &entity.OurServiceContentImage{ ID: imgID, OurServiceContentID: sid, ImageURL: imgURL, }) } _, err := _i.OurServiceImg.Save(&entity.OurServiceContentImage{ OurServiceContentID: sid, ImageURL: imgURL, }) return err } type partnerPayload struct { Action string `json:"action"` PartnerID string `json:"partner_id"` PrimaryTitle string `json:"primary_title"` ImagePath string `json:"image_path"` ImageURL string `json:"image_url"` } func (_i *cmsContentSubmissionsService) mergePartner(raw []byte) error { var p partnerPayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { if strings.TrimSpace(p.PartnerID) == "" { return errors.New("partner_id required for delete") } id, err := uuid.Parse(p.PartnerID) if err != nil { return err } return _i.Partner.Delete(id) } ent := &entity.PartnerContent{ PrimaryTitle: strings.TrimSpace(p.PrimaryTitle), ImagePath: p.ImagePath, ImageURL: strings.TrimSpace(p.ImageURL), } if strings.TrimSpace(p.PartnerID) == "" { _, err := _i.Partner.Save(ent) return err } id, err := uuid.Parse(p.PartnerID) if err != nil { return err } return _i.Partner.Update(id, ent) } type popupPayload struct { Action string `json:"action"` PopupID *uint `json:"popup_id"` PrimaryTitle string `json:"primary_title"` SecondaryTitle string `json:"secondary_title"` Description string `json:"description"` PrimaryCta string `json:"primary_cta"` SecondaryCtaText string `json:"secondary_cta_text"` MediaURL string `json:"media_url"` } func (_i *cmsContentSubmissionsService) mergePopup(raw []byte) error { var p popupPayload if err := json.Unmarshal(raw, &p); err != nil { return err } if strings.EqualFold(p.Action, "delete") { if p.PopupID == nil || *p.PopupID == 0 { return errors.New("popup_id required for delete") } return _i.Popup.Delete(*p.PopupID) } if p.PopupID == nil || *p.PopupID == 0 { res, err := _i.Popup.Save(popupNewsReq.PopupNewsContentsCreateRequest{ PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, PrimaryCTA: p.PrimaryCta, SecondaryCTAText: p.SecondaryCtaText, }) if err != nil { return err } return _i.attachPopupImage(res.ID, p.MediaURL) } pid := *p.PopupID if err := _i.Popup.Update(pid, popupNewsReq.PopupNewsContentsUpdateRequest{ ID: pid, PrimaryTitle: p.PrimaryTitle, SecondaryTitle: p.SecondaryTitle, Description: p.Description, PrimaryCTA: p.PrimaryCta, SecondaryCTAText: p.SecondaryCtaText, }); err != nil { return err } return _i.attachPopupImage(pid, p.MediaURL) } func (_i *cmsContentSubmissionsService) attachPopupImage(popupID uint, mediaURL string) error { mediaURL = strings.TrimSpace(mediaURL) if mediaURL == "" { return nil } return _i.PopupImg.Save(popupImageReq.PopupNewsContentImagesCreateRequest{ PopupNewsContentID: popupID, MediaPath: "", MediaURL: mediaURL, }) }