first commit
This commit is contained in:
97
internal/modules/seriessetmaster/dto.go
Normal file
97
internal/modules/seriessetmaster/dto.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cardverse/internal/pkg/utils"
|
||||
)
|
||||
|
||||
type ListSeriesQuery struct {
|
||||
Search string `form:"search"`
|
||||
}
|
||||
|
||||
type ListSetQuery struct {
|
||||
Search string `form:"search"`
|
||||
ParentID uint `form:"parent_id"`
|
||||
}
|
||||
|
||||
type CreateSeriesRequest struct {
|
||||
Name string `json:"name" binding:"required,min=2,max=150"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
}
|
||||
|
||||
type CreateSetRequest struct {
|
||||
ParentID uint `json:"parent_id" binding:"required"`
|
||||
SetCode string `json:"set_code" binding:"required,max=50"`
|
||||
SetName string `json:"set_name" binding:"required,max=150"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
Logo string `json:"logo" binding:"omitempty,max=255"`
|
||||
ReleaseDate *time.Time `json:"release_date" binding:"omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSeriesRequest struct {
|
||||
Name string `json:"name" binding:"omitempty,min=2,max=150"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
}
|
||||
|
||||
type UpdateSetRequest struct {
|
||||
ParentID *uint `json:"parent_id" binding:"omitempty"`
|
||||
SetCode string `json:"set_code" binding:"omitempty,max=50"`
|
||||
SetName string `json:"set_name" binding:"omitempty,max=150"`
|
||||
Image string `json:"image" binding:"omitempty,max=255"`
|
||||
Logo string `json:"logo" binding:"omitempty,max=255"`
|
||||
ReleaseDate *time.Time `json:"release_date" binding:"omitempty"`
|
||||
}
|
||||
|
||||
type SeriesSetMasterResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ParentID *uint `json:"parent_id,omitempty"`
|
||||
SetCode string `json:"set_code,omitempty"`
|
||||
SetName string `json:"set_name,omitempty"`
|
||||
SeriesName string `json:"series_name"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
ReleaseDate *time.Time `json:"release_date,omitempty"`
|
||||
TotalCards int `json:"total_cards"`
|
||||
ExpansionCount int `json:"expansion_count"`
|
||||
CardCount int `json:"card_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy *uint `json:"created_by,omitempty"`
|
||||
UpdatedBy *uint `json:"updated_by,omitempty"`
|
||||
Children []SeriesSetMasterResponse `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func ToSeriesSetMasterResponse(m SeriesSetMaster) SeriesSetMasterResponse {
|
||||
var children []SeriesSetMasterResponse
|
||||
if len(m.Children) > 0 {
|
||||
children = ToSeriesSetMasterResponseList(m.Children)
|
||||
}
|
||||
|
||||
return SeriesSetMasterResponse{
|
||||
ID: m.ID,
|
||||
ParentID: m.ParentID,
|
||||
SetCode: m.SetCode,
|
||||
SetName: m.SetName,
|
||||
SeriesName: m.SeriesName,
|
||||
Image: utils.FormatMediaURL(m.Image),
|
||||
Logo: utils.FormatMediaURL(m.Logo),
|
||||
ReleaseDate: m.ReleaseDate,
|
||||
TotalCards: m.TotalCards,
|
||||
ExpansionCount: m.ExpansionCount,
|
||||
CardCount: m.CardCount,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
CreatedBy: m.CreatedBy,
|
||||
UpdatedBy: m.UpdatedBy,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func ToSeriesSetMasterResponseList(list []SeriesSetMaster) []SeriesSetMasterResponse {
|
||||
res := make([]SeriesSetMasterResponse, len(list))
|
||||
for i, m := range list {
|
||||
res[i] = ToSeriesSetMasterResponse(m)
|
||||
}
|
||||
return res
|
||||
}
|
||||
550
internal/modules/seriessetmaster/handler.go
Normal file
550
internal/modules/seriessetmaster/handler.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cardverse/internal/pkg/response"
|
||||
"cardverse/internal/pkg/utils"
|
||||
"cardverse/internal/pkg/validator"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service Service
|
||||
}
|
||||
|
||||
func NewHandler(service Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func (h *Handler) GetAllSeries(c *gin.Context) {
|
||||
var query ListSeriesQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "query parameter tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
|
||||
list, err := h.service.GetAllSeries(query)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengambil data series", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data series", ToSeriesSetMasterResponseList(list))
|
||||
}
|
||||
|
||||
func (h *Handler) GetAllSets(c *gin.Context) {
|
||||
var query ListSetQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "query parameter tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
|
||||
list, err := h.service.GetAllSets(query)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengambil data set", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data set", ToSeriesSetMasterResponseList(list))
|
||||
}
|
||||
|
||||
func (h *Handler) GetByID(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
m, err := h.service.GetByID(id)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data", ToSeriesSetMasterResponse(*m))
|
||||
}
|
||||
|
||||
func (h *Handler) GetSetByCode(c *gin.Context) {
|
||||
code := c.Param("code")
|
||||
if code == "" {
|
||||
response.Error(c, http.StatusBadRequest, "set_code tidak boleh kosong", nil)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := h.service.GetSetByCode(code)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil mengambil data set", ToSeriesSetMasterResponse(*m))
|
||||
}
|
||||
|
||||
func (h *Handler) CreateSeries(c *gin.Context) {
|
||||
var req CreateSeriesRequest
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
req.Name = c.PostForm("name")
|
||||
if req.Name == "" {
|
||||
response.Error(c, http.StatusBadRequest, "name wajib diisi", nil)
|
||||
return
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size > 5*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err == nil {
|
||||
defer srcFile.Close()
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err == nil {
|
||||
slug := utils.UnderscoreString(req.Name)
|
||||
uploadDir := "./public/images/series"
|
||||
_ = os.MkdirAll(uploadDir, os.ModePerm)
|
||||
filename := fmt.Sprintf("%s.webp", slug)
|
||||
filepathDst := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(filepathDst)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil {
|
||||
req.Image = fmt.Sprintf("/images/series/%s", filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
created, err := h.service.CreateSeries(req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "series berhasil dibuat", ToSeriesSetMasterResponse(*created))
|
||||
}
|
||||
|
||||
func (h *Handler) CreateSet(c *gin.Context) {
|
||||
var req CreateSetRequest
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
if parentIDStr := c.PostForm("parent_id"); parentIDStr != "" {
|
||||
if pID, err := strconv.ParseUint(parentIDStr, 10, 64); err == nil {
|
||||
req.ParentID = uint(pID)
|
||||
}
|
||||
}
|
||||
req.SetCode = c.PostForm("set_code")
|
||||
req.SetName = c.PostForm("set_name")
|
||||
|
||||
if req.ParentID == 0 || req.SetCode == "" || req.SetName == "" {
|
||||
response.Error(c, http.StatusBadRequest, "parent_id, set_code, dan set_name wajib diisi", nil)
|
||||
return
|
||||
}
|
||||
|
||||
parentSeries, err := h.service.GetByID(req.ParentID)
|
||||
if err != nil || parentSeries == nil || parentSeries.ParentID != nil {
|
||||
response.Error(c, http.StatusBadRequest, "parent_id tidak ditemukan atau bukan merupakan series", nil)
|
||||
return
|
||||
}
|
||||
|
||||
seriesSlug := utils.Slugify(parentSeries.SeriesName)
|
||||
uploadDir := fmt.Sprintf("./public/images/%s/%s", seriesSlug, req.SetCode)
|
||||
|
||||
// Handle file 'image'
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size <= 5*1024*1024 {
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err == nil {
|
||||
defer srcFile.Close()
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err == nil {
|
||||
_ = os.MkdirAll(uploadDir, os.ModePerm)
|
||||
imgSlug := utils.UnderscoreString(req.SetName)
|
||||
filename := fmt.Sprintf("%s.webp", imgSlug)
|
||||
filepathDst := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(filepathDst)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil {
|
||||
req.Image = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file 'logo'
|
||||
logoHeader, err := c.FormFile("logo")
|
||||
if err == nil && logoHeader != nil {
|
||||
if logoHeader.Size <= 5*1024*1024 {
|
||||
_ = os.MkdirAll(uploadDir, os.ModePerm)
|
||||
origFilename := logoHeader.Filename
|
||||
ext := filepath.Ext(origFilename)
|
||||
baseName := strings.TrimSuffix(origFilename, ext)
|
||||
if baseName == "" {
|
||||
baseName = req.SetCode
|
||||
}
|
||||
|
||||
var logoFilename string
|
||||
if (len(req.SetCode) > 5 || len(baseName) > 5) && (strings.Contains(req.SetCode, "-P") || strings.Contains(baseName, "-P")) {
|
||||
logoFilename = "PROMO.svg"
|
||||
} else {
|
||||
logoFilename = fmt.Sprintf("%s.svg", baseName)
|
||||
}
|
||||
|
||||
filepathDstLogo := filepath.Join(uploadDir, logoFilename)
|
||||
|
||||
srcLogo, err := logoHeader.Open()
|
||||
if err == nil {
|
||||
defer srcLogo.Close()
|
||||
logoBytes, err := io.ReadAll(srcLogo)
|
||||
if err == nil {
|
||||
checkLen := len(logoBytes)
|
||||
if checkLen > 512 {
|
||||
checkLen = 512
|
||||
}
|
||||
isSVG := strings.EqualFold(ext, ".svg") || strings.Contains(strings.ToLower(string(logoBytes[:checkLen])), "<svg")
|
||||
|
||||
if isSVG {
|
||||
if err := os.WriteFile(filepathDstLogo, logoBytes, 0644); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, logoFilename)
|
||||
}
|
||||
} else {
|
||||
img, _, err := image.Decode(bytes.NewReader(logoBytes))
|
||||
if err == nil && img != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err == nil {
|
||||
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
svgContent := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="0 0 %d %d"><image width="%d" height="%d" xlink:href="data:image/png;base64,%s"/></svg>`, w, h, w, h, w, h, b64)
|
||||
|
||||
if err := os.WriteFile(filepathDstLogo, []byte(svgContent), 0644); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, logoFilename)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.SaveUploadedFile(logoHeader, filepathDstLogo); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, req.SetCode, logoFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
created, err := h.service.CreateSet(req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "set berhasil dibuat", ToSeriesSetMasterResponse(*created))
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateSeries(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateSeriesRequest
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
req.Name = c.PostForm("name")
|
||||
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size > 5*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membaca file gambar series", err.Error())
|
||||
return
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "file yang diunggah bukan format gambar yang valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
seriesName := req.Name
|
||||
if seriesName == "" {
|
||||
m, err := h.service.GetByID(id)
|
||||
if err == nil && m != nil {
|
||||
seriesName = m.SeriesName
|
||||
}
|
||||
}
|
||||
|
||||
slug := utils.UnderscoreString(seriesName)
|
||||
|
||||
uploadDir := "./public/images/series"
|
||||
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat direktori penyimpanan series", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s.webp", slug)
|
||||
filepathDst := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(filepathDst)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal membuat file gambar series", err.Error())
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "gagal mengompresi gambar ke format WebP", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.Image = fmt.Sprintf("/images/series/%s", filename)
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := h.service.UpdateSeries(id, req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "series berhasil diupdate", ToSeriesSetMasterResponse(*updated))
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateSet(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
mOld, err := h.service.GetByID(id)
|
||||
if err != nil || mOld == nil {
|
||||
response.Error(c, http.StatusNotFound, "data set tidak ditemukan", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateSetRequest
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
req.SetCode = c.PostForm("set_code")
|
||||
req.SetName = c.PostForm("set_name")
|
||||
|
||||
if parentIDStr := c.PostForm("parent_id"); parentIDStr != "" {
|
||||
if pID, err := strconv.ParseUint(parentIDStr, 10, 64); err == nil {
|
||||
uintPID := uint(pID)
|
||||
req.ParentID = &uintPID
|
||||
}
|
||||
}
|
||||
|
||||
setCode := req.SetCode
|
||||
if setCode == "" {
|
||||
setCode = mOld.SetCode
|
||||
}
|
||||
setName := req.SetName
|
||||
if setName == "" {
|
||||
setName = mOld.SetName
|
||||
}
|
||||
|
||||
seriesName := mOld.SeriesName
|
||||
if req.ParentID != nil {
|
||||
parentSeries, err := h.service.GetByID(*req.ParentID)
|
||||
if err == nil && parentSeries != nil && parentSeries.ParentID == nil {
|
||||
seriesName = parentSeries.SeriesName
|
||||
}
|
||||
}
|
||||
|
||||
seriesSlug := utils.Slugify(seriesName)
|
||||
uploadDir := fmt.Sprintf("./public/images/%s/%s", seriesSlug, setCode)
|
||||
|
||||
// 1. Handle file 'image' (WebP format, nama file = utils.UnderscoreString(setName) + ".webp")
|
||||
fileHeader, err := c.FormFile("image")
|
||||
if err == nil && fileHeader != nil {
|
||||
if fileHeader.Size > 5*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file gambar maksimal 5MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
srcFile, err := fileHeader.Open()
|
||||
if err == nil {
|
||||
defer srcFile.Close()
|
||||
img, _, err := image.Decode(srcFile)
|
||||
if err == nil {
|
||||
_ = os.MkdirAll(uploadDir, os.ModePerm)
|
||||
imgSlug := utils.UnderscoreString(setName)
|
||||
filename := fmt.Sprintf("%s.webp", imgSlug)
|
||||
filepathDst := filepath.Join(uploadDir, filename)
|
||||
|
||||
out, err := os.Create(filepathDst)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 90}); err == nil {
|
||||
req.Image = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Handle file 'logo' (Format SVG: file SVG disimpan langsung, JPG/PNG/GIF/WebP dikonversi ke SVG XML)
|
||||
logoHeader, err := c.FormFile("logo")
|
||||
if err == nil && logoHeader != nil {
|
||||
if logoHeader.Size > 5*1024*1024 {
|
||||
response.Error(c, http.StatusBadRequest, "ukuran file logo maksimal 5MB", nil)
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.MkdirAll(uploadDir, os.ModePerm)
|
||||
|
||||
origFilename := logoHeader.Filename
|
||||
ext := filepath.Ext(origFilename)
|
||||
baseName := strings.TrimSuffix(origFilename, ext)
|
||||
if baseName == "" {
|
||||
baseName = setCode
|
||||
}
|
||||
|
||||
var logoFilename string
|
||||
if (len(setCode) > 5 || len(baseName) > 5) && (strings.Contains(setCode, "-P") || strings.Contains(baseName, "-P")) {
|
||||
logoFilename = "PROMO.svg"
|
||||
} else {
|
||||
logoFilename = fmt.Sprintf("%s.svg", baseName)
|
||||
}
|
||||
|
||||
filepathDstLogo := filepath.Join(uploadDir, logoFilename)
|
||||
|
||||
srcLogo, err := logoHeader.Open()
|
||||
if err == nil {
|
||||
defer srcLogo.Close()
|
||||
logoBytes, err := io.ReadAll(srcLogo)
|
||||
if err == nil {
|
||||
checkLen := len(logoBytes)
|
||||
if checkLen > 512 {
|
||||
checkLen = 512
|
||||
}
|
||||
isSVG := strings.EqualFold(ext, ".svg") || strings.Contains(strings.ToLower(string(logoBytes[:checkLen])), "<svg")
|
||||
|
||||
if isSVG {
|
||||
if err := os.WriteFile(filepathDstLogo, logoBytes, 0644); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, logoFilename)
|
||||
}
|
||||
} else {
|
||||
img, _, err := image.Decode(bytes.NewReader(logoBytes))
|
||||
if err == nil && img != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err == nil {
|
||||
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
svgContent := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="0 0 %d %d"><image width="%d" height="%d" xlink:href="data:image/png;base64,%s"/></svg>`, w, h, w, h, w, h, b64)
|
||||
|
||||
if err := os.WriteFile(filepathDstLogo, []byte(svgContent), 0644); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, logoFilename)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.SaveUploadedFile(logoHeader, filepathDstLogo); err == nil {
|
||||
req.Logo = fmt.Sprintf("/images/%s/%s/%s", seriesSlug, setCode, logoFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := h.service.UpdateSet(id, req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "set berhasil diupdate", ToSeriesSetMasterResponse(*updated))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "id tidak valid", []validator.FieldError{
|
||||
{Field: "id", Message: "id harus berupa angka"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Delete(id); err != nil {
|
||||
response.Error(c, http.StatusNotFound, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "berhasil menghapus data series/set", nil)
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint, error) {
|
||||
idParam := c.Param("id")
|
||||
id, err := strconv.ParseUint(idParam, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
60
internal/modules/seriessetmaster/model.go
Normal file
60
internal/modules/seriessetmaster/model.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package seriessetmaster
|
||||
|
||||
import "time"
|
||||
|
||||
// SeriesSetMaster merepresentasikan tabel utama "series_set_masters" di database (digunakan untuk AutoMigrate GORM)
|
||||
type SeriesSetMaster struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ParentID *uint `json:"parent_id" gorm:"index;default:null"`
|
||||
SetCode string `json:"set_code,omitempty" gorm:"type:varchar(50);index"`
|
||||
SetName string `json:"set_name,omitempty" gorm:"type:varchar(150)"`
|
||||
SeriesName string `json:"series_name" gorm:"type:varchar(150);not null;index"`
|
||||
Image string `json:"image" gorm:"type:varchar(255)"`
|
||||
Logo string `json:"logo,omitempty" gorm:"type:varchar(255)"`
|
||||
ReleaseDate *time.Time `json:"release_date,omitempty" gorm:"type:date"`
|
||||
TotalCards int `json:"total_cards" gorm:"default:0"`
|
||||
ExpansionCount int `json:"expansion_count" gorm:"default:0"`
|
||||
CardCount int `json:"card_count" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
CreatedBy *uint `json:"created_by" gorm:"default:null"`
|
||||
UpdatedBy *uint `json:"updated_by" gorm:"default:null"`
|
||||
Parent *SeriesSetMaster `json:"parent,omitempty" gorm:"foreignKey:ParentID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:RESTRICT;"`
|
||||
Children []SeriesSetMaster `json:"children,omitempty" gorm:"foreignKey:ParentID;references:ID"`
|
||||
}
|
||||
|
||||
func (SeriesSetMaster) TableName() string {
|
||||
return "series_set_masters"
|
||||
}
|
||||
|
||||
// Series merepresentasikan data khusus Series untuk kelola data/response
|
||||
type Series struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ExpansionCount int `json:"expansion_count"`
|
||||
CardCount int `json:"card_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy *uint `json:"created_by"`
|
||||
UpdatedBy *uint `json:"updated_by"`
|
||||
Sets []Set `json:"sets,omitempty"`
|
||||
}
|
||||
|
||||
// Set merepresentasikan data khusus Set untuk kelola data/response
|
||||
type Set struct {
|
||||
ID uint `json:"id"`
|
||||
ParentID uint `json:"parent_id"`
|
||||
SetCode string `json:"set_code"`
|
||||
SetName string `json:"set_name"`
|
||||
SeriesName string `json:"series_name"`
|
||||
Image string `json:"image"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
ReleaseDate *time.Time `json:"release_date,omitempty"`
|
||||
TotalCards int `json:"total_cards"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy *uint `json:"created_by"`
|
||||
UpdatedBy *uint `json:"updated_by"`
|
||||
Series *Series `json:"series,omitempty"`
|
||||
}
|
||||
228
internal/modules/seriessetmaster/repository.go
Normal file
228
internal/modules/seriessetmaster/repository.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
FindAllSeries(search string) ([]SeriesSetMaster, error)
|
||||
FindAllSets(search string, parentID uint) ([]SeriesSetMaster, error)
|
||||
FindByID(id uint) (*SeriesSetMaster, error)
|
||||
FindBySetCode(setCode string) (*SeriesSetMaster, error)
|
||||
FindCardImagesBySetID(setID uint) ([]string, error)
|
||||
Create(m *SeriesSetMaster) error
|
||||
CreateSet(m *SeriesSetMaster) error
|
||||
Update(m *SeriesSetMaster) error
|
||||
UpdateSet(m *SeriesSetMaster, oldParentID *uint) error
|
||||
DeleteCascade(m *SeriesSetMaster) error
|
||||
RecalculateSeriesStats(seriesID uint) error
|
||||
RecalculateSetStats(setID uint) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) FindAllSeries(search string) ([]SeriesSetMaster, error) {
|
||||
var list []SeriesSetMaster
|
||||
|
||||
query := r.db.Model(&SeriesSetMaster{}).
|
||||
Select("id", "parent_id", "series_name", "image", "expansion_count", "card_count", "created_at", "updated_at", "created_by", "updated_by").
|
||||
Where("parent_id IS NULL")
|
||||
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
query = query.Where("series_name ILIKE ?", pattern)
|
||||
}
|
||||
|
||||
err := query.Order("id ASC").Preload("Children").Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindAllSets(search string, parentID uint) ([]SeriesSetMaster, error) {
|
||||
var list []SeriesSetMaster
|
||||
|
||||
query := r.db.Model(&SeriesSetMaster{}).
|
||||
Select("id", "parent_id", "set_code", "set_name", "series_name", "image", "logo", "release_date", "total_cards", "created_at", "updated_at", "created_by", "updated_by").
|
||||
Where("parent_id IS NOT NULL")
|
||||
|
||||
if parentID > 0 {
|
||||
query = query.Where("parent_id = ?", parentID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
query = query.Where("set_name ILIKE ? OR set_code ILIKE ? OR series_name ILIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
|
||||
err := query.Order("id ASC").Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindByID(id uint) (*SeriesSetMaster, error) {
|
||||
var m SeriesSetMaster
|
||||
err := r.db.Select("id", "parent_id", "set_code", "set_name", "series_name", "image", "logo", "release_date", "total_cards", "expansion_count", "card_count", "created_at", "updated_at", "created_by", "updated_by").
|
||||
Preload("Children").
|
||||
First(&m, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindBySetCode(setCode string) (*SeriesSetMaster, error) {
|
||||
var m SeriesSetMaster
|
||||
err := r.db.Select("id", "parent_id", "set_code", "set_name", "series_name", "image", "logo", "release_date", "total_cards", "expansion_count", "card_count", "created_at", "updated_at", "created_by", "updated_by").
|
||||
Where("set_code = ? AND parent_id IS NOT NULL", setCode).
|
||||
First(&m).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindCardImagesBySetID(setID uint) ([]string, error) {
|
||||
var images []string
|
||||
err := r.db.Table("card_masters").Select("image").Where("id_set = ? AND image != ''", setID).Pluck("image", &images).Error
|
||||
return images, err
|
||||
}
|
||||
|
||||
func (r *repository) Create(m *SeriesSetMaster) error {
|
||||
return r.db.Create(m).Error
|
||||
}
|
||||
|
||||
func (r *repository) CreateSet(m *SeriesSetMaster) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(m).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if m.ParentID != nil {
|
||||
return r.RecalculateSeriesStatsTx(tx, *m.ParentID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) Update(m *SeriesSetMaster) error {
|
||||
return r.db.Save(m).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateSet(m *SeriesSetMaster, oldParentID *uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(m).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if m.ParentID != nil {
|
||||
if err := r.RecalculateSeriesStatsTx(tx, *m.ParentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if oldParentID != nil && (m.ParentID == nil || *oldParentID != *m.ParentID) {
|
||||
if err := r.RecalculateSeriesStatsTx(tx, *oldParentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) DeleteCascade(m *SeriesSetMaster) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
parentID := m.ParentID
|
||||
if m.ParentID == nil {
|
||||
if err := tx.Table("card_masters").Where("id_set IN (SELECT id FROM series_set_masters WHERE parent_id = ?)", m.ID).Delete(nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("parent_id = ?", m.ID).Delete(&SeriesSetMaster{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&SeriesSetMaster{}, m.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Table("card_masters").Where("id_set = ?", m.ID).Delete(nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&SeriesSetMaster{}, m.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if parentID != nil {
|
||||
if err := r.RecalculateSeriesStatsTx(tx, *parentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) RecalculateSeriesStats(seriesID uint) error {
|
||||
return r.RecalculateSeriesStatsTx(r.db, seriesID)
|
||||
}
|
||||
|
||||
func (r *repository) RecalculateSeriesStatsTx(tx *gorm.DB, seriesID uint) error {
|
||||
if seriesID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expansionCount int64
|
||||
if err := tx.Model(&SeriesSetMaster{}).Where("parent_id = ?", seriesID).Count(&expansionCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cardCount int64
|
||||
if err := tx.Table("card_masters").
|
||||
Where("id_set IN (SELECT id FROM series_set_masters WHERE parent_id = ?)", seriesID).
|
||||
Count(&cardCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Model(&SeriesSetMaster{}).
|
||||
Where("id = ? AND parent_id IS NULL", seriesID).
|
||||
Updates(map[string]interface{}{
|
||||
"expansion_count": int(expansionCount),
|
||||
"card_count": int(cardCount),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *repository) RecalculateSetStats(setID uint) error {
|
||||
return r.RecalculateSetStatsTx(r.db, setID)
|
||||
}
|
||||
|
||||
func (r *repository) RecalculateSetStatsTx(tx *gorm.DB, setID uint) error {
|
||||
if setID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var set SeriesSetMaster
|
||||
if err := tx.Select("id", "parent_id").First(&set, setID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var totalCards int64
|
||||
if err := tx.Table("card_masters").Where("id_set = ?", setID).Count(&totalCards).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&SeriesSetMaster{}).Where("id = ?", setID).Update("total_cards", int(totalCards)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if set.ParentID != nil {
|
||||
return r.RecalculateSeriesStatsTx(tx, *set.ParentID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
internal/modules/seriessetmaster/repository_mock_test.go
Normal file
100
internal/modules/seriessetmaster/repository_mock_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package seriessetmaster
|
||||
|
||||
type mockRepository struct {
|
||||
findAllSeriesFunc func(search string) ([]SeriesSetMaster, error)
|
||||
findAllSetsFunc func(search string, parentID uint) ([]SeriesSetMaster, error)
|
||||
findByIDFunc func(id uint) (*SeriesSetMaster, error)
|
||||
findBySetCodeFunc func(setCode string) (*SeriesSetMaster, error)
|
||||
findCardImagesBySetIDFunc func(setID uint) ([]string, error)
|
||||
createFunc func(m *SeriesSetMaster) error
|
||||
createSetFunc func(m *SeriesSetMaster) error
|
||||
updateFunc func(m *SeriesSetMaster) error
|
||||
updateSetFunc func(m *SeriesSetMaster, oldParentID *uint) error
|
||||
deleteCascadeFunc func(m *SeriesSetMaster) error
|
||||
recalculateSeriesStatsFunc func(seriesID uint) error
|
||||
recalculateSetStatsFunc func(setID uint) error
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindAllSeries(search string) ([]SeriesSetMaster, error) {
|
||||
if m.findAllSeriesFunc != nil {
|
||||
return m.findAllSeriesFunc(search)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindAllSets(search string, parentID uint) ([]SeriesSetMaster, error) {
|
||||
if m.findAllSetsFunc != nil {
|
||||
return m.findAllSetsFunc(search, parentID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindByID(id uint) (*SeriesSetMaster, error) {
|
||||
if m.findByIDFunc != nil {
|
||||
return m.findByIDFunc(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindBySetCode(setCode string) (*SeriesSetMaster, error) {
|
||||
if m.findBySetCodeFunc != nil {
|
||||
return m.findBySetCodeFunc(setCode)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) FindCardImagesBySetID(setID uint) ([]string, error) {
|
||||
if m.findCardImagesBySetIDFunc != nil {
|
||||
return m.findCardImagesBySetIDFunc(setID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) Create(item *SeriesSetMaster) error {
|
||||
if m.createFunc != nil {
|
||||
return m.createFunc(item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) CreateSet(item *SeriesSetMaster) error {
|
||||
if m.createSetFunc != nil {
|
||||
return m.createSetFunc(item)
|
||||
}
|
||||
return m.Create(item)
|
||||
}
|
||||
|
||||
func (m *mockRepository) Update(item *SeriesSetMaster) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) UpdateSet(item *SeriesSetMaster, oldParentID *uint) error {
|
||||
if m.updateSetFunc != nil {
|
||||
return m.updateSetFunc(item, oldParentID)
|
||||
}
|
||||
return m.Update(item)
|
||||
}
|
||||
|
||||
func (m *mockRepository) DeleteCascade(item *SeriesSetMaster) error {
|
||||
if m.deleteCascadeFunc != nil {
|
||||
return m.deleteCascadeFunc(item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) RecalculateSeriesStats(seriesID uint) error {
|
||||
if m.recalculateSeriesStatsFunc != nil {
|
||||
return m.recalculateSeriesStatsFunc(seriesID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRepository) RecalculateSetStats(setID uint) error {
|
||||
if m.recalculateSetStatsFunc != nil {
|
||||
return m.recalculateSetStatsFunc(setID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
36
internal/modules/seriessetmaster/routes.go
Normal file
36
internal/modules/seriessetmaster/routes.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"cardverse/internal/middleware"
|
||||
"cardverse/internal/modules/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(router *gin.RouterGroup, db *gorm.DB) {
|
||||
repo := NewRepository(db)
|
||||
svc := NewService(repo)
|
||||
handler := NewHandler(svc)
|
||||
|
||||
series := router.Group("/series")
|
||||
{
|
||||
series.GET("", handler.GetAllSeries)
|
||||
series.POST("", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.CreateSeries)
|
||||
series.PUT("/:id", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.UpdateSeries)
|
||||
}
|
||||
|
||||
sets := router.Group("/sets")
|
||||
{
|
||||
sets.GET("", handler.GetAllSets)
|
||||
sets.GET("/code/:code", handler.GetSetByCode)
|
||||
sets.POST("", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.CreateSet)
|
||||
sets.PUT("/:id", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.UpdateSet)
|
||||
}
|
||||
|
||||
master := router.Group("/series-set-masters")
|
||||
{
|
||||
master.GET("/:id", handler.GetByID)
|
||||
master.DELETE("/:id", middleware.AuthRequired(), middleware.RequireRoles(user.RoleAdmin, user.RoleSuperAdmin), handler.Delete)
|
||||
}
|
||||
}
|
||||
206
internal/modules/seriessetmaster/service.go
Normal file
206
internal/modules/seriessetmaster/service.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"cardverse/internal/pkg/utils"
|
||||
)
|
||||
|
||||
var ErrSeriesSetNotFound = errors.New("data series atau set tidak ditemukan")
|
||||
|
||||
type Service interface {
|
||||
GetAllSeries(query ListSeriesQuery) ([]SeriesSetMaster, error)
|
||||
GetAllSets(query ListSetQuery) ([]SeriesSetMaster, error)
|
||||
GetByID(id uint) (*SeriesSetMaster, error)
|
||||
GetSetByCode(code string) (*SeriesSetMaster, error)
|
||||
CreateSeries(req CreateSeriesRequest) (*SeriesSetMaster, error)
|
||||
CreateSet(req CreateSetRequest) (*SeriesSetMaster, error)
|
||||
UpdateSeries(id uint, req UpdateSeriesRequest) (*SeriesSetMaster, error)
|
||||
UpdateSet(id uint, req UpdateSetRequest) (*SeriesSetMaster, error)
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetAllSeries(query ListSeriesQuery) ([]SeriesSetMaster, error) {
|
||||
return s.repo.FindAllSeries(query.Search)
|
||||
}
|
||||
|
||||
func (s *service) GetAllSets(query ListSetQuery) ([]SeriesSetMaster, error) {
|
||||
return s.repo.FindAllSets(query.Search, query.ParentID)
|
||||
}
|
||||
|
||||
func (s *service) GetByID(id uint) (*SeriesSetMaster, error) {
|
||||
m, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, ErrSeriesSetNotFound
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) GetSetByCode(code string) (*SeriesSetMaster, error) {
|
||||
m, err := s.repo.FindBySetCode(code)
|
||||
if err != nil {
|
||||
return nil, ErrSeriesSetNotFound
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) CreateSeries(req CreateSeriesRequest) (*SeriesSetMaster, error) {
|
||||
m := &SeriesSetMaster{
|
||||
SeriesName: req.Name,
|
||||
Image: req.Image,
|
||||
ParentID: nil,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) CreateSet(req CreateSetRequest) (*SeriesSetMaster, error) {
|
||||
parent, err := s.repo.FindByID(req.ParentID)
|
||||
if err != nil || parent == nil || parent.ParentID != nil {
|
||||
return nil, errors.New("parent_id tidak ditemukan atau bukan merupakan series")
|
||||
}
|
||||
|
||||
m := &SeriesSetMaster{
|
||||
ParentID: &req.ParentID,
|
||||
SetCode: req.SetCode,
|
||||
SetName: req.SetName,
|
||||
SeriesName: parent.SeriesName,
|
||||
Image: req.Image,
|
||||
Logo: req.Logo,
|
||||
ReleaseDate: req.ReleaseDate,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateSet(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) UpdateSeries(id uint, req UpdateSeriesRequest) (*SeriesSetMaster, error) {
|
||||
m, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, ErrSeriesSetNotFound
|
||||
}
|
||||
|
||||
if m.ParentID != nil {
|
||||
return nil, errors.New("record ini adalah set, bukan series")
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
m.SeriesName = req.Name
|
||||
}
|
||||
if req.Image != "" {
|
||||
if m.Image != "" && m.Image != req.Image {
|
||||
utils.DeleteLocalFile(m.Image)
|
||||
}
|
||||
m.Image = req.Image
|
||||
}
|
||||
|
||||
if err := s.repo.Update(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) UpdateSet(id uint, req UpdateSetRequest) (*SeriesSetMaster, error) {
|
||||
m, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, ErrSeriesSetNotFound
|
||||
}
|
||||
|
||||
if m.ParentID == nil {
|
||||
return nil, errors.New("record ini adalah series, bukan set")
|
||||
}
|
||||
|
||||
var oldParentID *uint
|
||||
if req.ParentID != nil && (m.ParentID == nil || *req.ParentID != *m.ParentID) {
|
||||
oldParentID = m.ParentID
|
||||
parent, err := s.repo.FindByID(*req.ParentID)
|
||||
if err != nil || parent == nil || parent.ParentID != nil {
|
||||
return nil, errors.New("parent_id tidak ditemukan atau bukan merupakan series")
|
||||
}
|
||||
m.ParentID = req.ParentID
|
||||
m.SeriesName = parent.SeriesName
|
||||
}
|
||||
|
||||
if req.SetCode != "" {
|
||||
m.SetCode = req.SetCode
|
||||
}
|
||||
if req.SetName != "" {
|
||||
m.SetName = req.SetName
|
||||
}
|
||||
if req.Image != "" {
|
||||
if m.Image != "" && m.Image != req.Image {
|
||||
utils.DeleteLocalFile(m.Image)
|
||||
}
|
||||
m.Image = req.Image
|
||||
}
|
||||
if req.Logo != "" {
|
||||
if m.Logo != "" && m.Logo != req.Logo {
|
||||
utils.DeleteLocalFile(m.Logo)
|
||||
}
|
||||
m.Logo = req.Logo
|
||||
}
|
||||
if req.ReleaseDate != nil {
|
||||
m.ReleaseDate = req.ReleaseDate
|
||||
}
|
||||
|
||||
if err := s.repo.UpdateSet(m, oldParentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(id uint) error {
|
||||
m, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
return ErrSeriesSetNotFound
|
||||
}
|
||||
|
||||
if m.ParentID == nil {
|
||||
childSets, _ := s.repo.FindAllSets("", m.ID)
|
||||
for _, set := range childSets {
|
||||
cardImages, _ := s.repo.FindCardImagesBySetID(set.ID)
|
||||
for _, imgPath := range cardImages {
|
||||
utils.DeleteLocalFile(imgPath)
|
||||
}
|
||||
if set.Image != "" {
|
||||
utils.DeleteLocalFile(set.Image)
|
||||
}
|
||||
if set.Logo != "" {
|
||||
utils.DeleteLocalFile(set.Logo)
|
||||
}
|
||||
}
|
||||
if m.Image != "" {
|
||||
utils.DeleteLocalFile(m.Image)
|
||||
}
|
||||
} else {
|
||||
cardImages, _ := s.repo.FindCardImagesBySetID(m.ID)
|
||||
for _, imgPath := range cardImages {
|
||||
utils.DeleteLocalFile(imgPath)
|
||||
}
|
||||
if m.Image != "" {
|
||||
utils.DeleteLocalFile(m.Image)
|
||||
}
|
||||
if m.Logo != "" {
|
||||
utils.DeleteLocalFile(m.Logo)
|
||||
}
|
||||
}
|
||||
|
||||
return s.repo.DeleteCascade(m)
|
||||
}
|
||||
226
internal/modules/seriessetmaster/service_test.go
Normal file
226
internal/modules/seriessetmaster/service_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package seriessetmaster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestService_GetAllSeries_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findAllSeriesFunc: func(search string) ([]SeriesSetMaster, error) {
|
||||
return []SeriesSetMaster{
|
||||
{ID: 1, SeriesName: "Evolusi Mega"},
|
||||
{ID: 2, SeriesName: "Scarlet & Violet"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
list, err := svc.GetAllSeries(ListSeriesQuery{Search: ""})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("expected 2 items, got: %d", len(list))
|
||||
}
|
||||
if list[0].SeriesName != "Evolusi Mega" {
|
||||
t.Errorf("expected 'Evolusi Mega', got: %s", list[0].SeriesName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetAllSets_Success(t *testing.T) {
|
||||
parentID := uint(1)
|
||||
repo := &mockRepository{
|
||||
findAllSetsFunc: func(search string, pID uint) ([]SeriesSetMaster, error) {
|
||||
return []SeriesSetMaster{
|
||||
{ID: 10, ParentID: &parentID, SetCode: "MA1", SetName: "Evolusi Mega"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
list, err := svc.GetAllSets(ListSetQuery{Search: "MA1", ParentID: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("expected 1 item, got: %d", len(list))
|
||||
}
|
||||
if list[0].SetCode != "MA1" {
|
||||
t.Errorf("expected 'MA1', got: %s", list[0].SetCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetByID_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return &SeriesSetMaster{ID: id, SeriesName: "Pedang & Perisai"}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
result, err := svc.GetByID(1)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if result.ID != 1 {
|
||||
t.Errorf("expected ID 1, got: %d", result.ID)
|
||||
}
|
||||
if result.SeriesName != "Pedang & Perisai" {
|
||||
t.Errorf("expected 'Pedang & Perisai', got: %s", result.SeriesName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetSetByCode_Success(t *testing.T) {
|
||||
parentID := uint(1)
|
||||
repo := &mockRepository{
|
||||
findBySetCodeFunc: func(code string) (*SeriesSetMaster, error) {
|
||||
return &SeriesSetMaster{ID: 10, ParentID: &parentID, SetCode: code, SetName: "Evolusi Mega M-P"}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
result, err := svc.GetSetByCode("M-P")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if result.SetCode != "M-P" {
|
||||
t.Errorf("expected 'M-P', got: %s", result.SetCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_GetByID_NotFound(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return nil, errors.New("record not found")
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.GetByID(999)
|
||||
if !errors.Is(err, ErrSeriesSetNotFound) {
|
||||
t.Fatalf("expected ErrSeriesSetNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSeries_Success(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return &SeriesSetMaster{ID: id, ParentID: nil, SeriesName: "Old Name"}, nil
|
||||
},
|
||||
updateFunc: func(m *SeriesSetMaster) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
req := UpdateSeriesRequest{
|
||||
Name: "New Series Name",
|
||||
Image: "/images/series/new.png",
|
||||
}
|
||||
|
||||
updated, err := svc.UpdateSeries(1, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if updated.SeriesName != "New Series Name" {
|
||||
t.Errorf("expected 'New Series Name', got: %s", updated.SeriesName)
|
||||
}
|
||||
if updated.Image != "/images/series/new.png" {
|
||||
t.Errorf("expected Image '/images/series/new.png', got: %s", updated.Image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSeries_NotFound(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return nil, errors.New("record not found")
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.UpdateSeries(999, UpdateSeriesRequest{Name: "Test"})
|
||||
if !errors.Is(err, ErrSeriesSetNotFound) {
|
||||
t.Fatalf("expected ErrSeriesSetNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSeries_InvalidRecordType(t *testing.T) {
|
||||
parentID := uint(1)
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return &SeriesSetMaster{ID: id, ParentID: &parentID, SetCode: "MA1"}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.UpdateSeries(1, UpdateSeriesRequest{Name: "Test"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when trying to update a Set using UpdateSeries, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSet_Success(t *testing.T) {
|
||||
parentID := uint(1)
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
if id == 1 {
|
||||
return &SeriesSetMaster{ID: 1, ParentID: nil, SeriesName: "Evolusi Mega"}, nil
|
||||
}
|
||||
return &SeriesSetMaster{ID: id, ParentID: &parentID, SetCode: "OLD", SetName: "Old Set", SeriesName: "Evolusi Mega"}, nil
|
||||
},
|
||||
updateFunc: func(m *SeriesSetMaster) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
now := time.Now()
|
||||
newParentID := uint(1)
|
||||
req := UpdateSetRequest{
|
||||
ParentID: &newParentID,
|
||||
SetCode: "NEW1",
|
||||
SetName: "New Set Name",
|
||||
ReleaseDate: &now,
|
||||
}
|
||||
|
||||
updated, err := svc.UpdateSet(10, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if updated.SetCode != "NEW1" {
|
||||
t.Errorf("expected 'NEW1', got: %s", updated.SetCode)
|
||||
}
|
||||
if updated.SetName != "New Set Name" {
|
||||
t.Errorf("expected 'New Set Name', got: %s", updated.SetName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSet_NotFound(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return nil, errors.New("record not found")
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.UpdateSet(999, UpdateSetRequest{SetName: "Test"})
|
||||
if !errors.Is(err, ErrSeriesSetNotFound) {
|
||||
t.Fatalf("expected ErrSeriesSetNotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateSet_InvalidRecordType(t *testing.T) {
|
||||
repo := &mockRepository{
|
||||
findByIDFunc: func(id uint) (*SeriesSetMaster, error) {
|
||||
return &SeriesSetMaster{ID: id, ParentID: nil, SeriesName: "Evolusi Mega"}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.UpdateSet(1, UpdateSetRequest{SetName: "Test"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when trying to update a Series using UpdateSet, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user