qudoco-be/app/module/cms_content_submissions/service/cms_content_submissions.ser...

603 lines
18 KiB
Go
Raw Normal View History

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"
userRoleAdmin = uint(1)
userRoleApprover = uint(2)
userRoleContributor = uint(3)
)
func canApproveCmsSubmissions(roleID uint) bool {
return roleID == userRoleApprover || roleID == userRoleAdmin
}
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.UserRoleId != userRoleContributor {
return nil, errors.New("only contributor role 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.UserRoleId == userRoleContributor {
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 !canApproveCmsSubmissions(user.UserRoleId) {
return errors.New("only approver or admin role 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 !canApproveCmsSubmissions(user.UserRoleId) {
return errors.New("only approver or admin role 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,
})
}