quzuu-api-test / controllers /academy_controller.go
lifedebugger's picture
Deploy files from GitHub repository
3577b8e
package controllers
import (
"errors"
"net/http"
"strconv"
"time"
"abdanhafidz.com/go-boilerplate/models/dto"
entity "abdanhafidz.com/go-boilerplate/models/entity"
http_error "abdanhafidz.com/go-boilerplate/models/error"
"abdanhafidz.com/go-boilerplate/services"
"abdanhafidz.com/go-boilerplate/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AcademyController interface {
// Academy
CreateAcademy(ctx *gin.Context)
GetAcademy(ctx *gin.Context)
GetAcademyDetail(ctx *gin.Context)
ListAcademy(ctx *gin.Context)
UpdateAcademy(ctx *gin.Context)
DeleteAcademy(ctx *gin.Context)
JoinAcademyByCode(ctx *gin.Context)
AssignAccountToAcademy(ctx *gin.Context)
UnassignAccountFromAcademy(ctx *gin.Context)
ListAssignmentsByAcademy(ctx *gin.Context)
// Material
GetMaterial(ctx *gin.Context)
ListMaterialsByAcademy(ctx *gin.Context)
GetMaterialDetail(ctx *gin.Context)
CreateMaterial(ctx *gin.Context)
UpdateMaterial(ctx *gin.Context)
DeleteMaterial(ctx *gin.Context)
// Content
CreateContent(ctx *gin.Context)
GetContent(ctx *gin.Context)
ListContentsByAcademy(ctx *gin.Context)
ListContentsByMaterial(ctx *gin.Context)
GetContentDetail(ctx *gin.Context)
UpdateContent(ctx *gin.Context)
DeleteContent(ctx *gin.Context)
// Progress
UpdateContentProgress(ctx *gin.Context)
}
type academyController struct {
academyService services.AcademyService
}
func NewAcademyController(academyService services.AcademyService) AcademyController {
return &academyController{academyService}
}
// ================= ACADEMY =================
// GetAcademy godoc
// @Summary Get Academy by Slug
// @Description Retrieve academy details using its slug
// @Tags Academy
// @Accept json
// @Produce json
// @Param academy_slug path string true "Academy Slug"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/academy/{academy_slug} [get]
func (c *academyController) GetAcademy(ctx *gin.Context) {
academySlug := ctx.Param("academy_slug")
accountId := ParseAccountId(ctx)
res, err := c.academyService.GetAcademyResponse(ctx.Request.Context(), accountId, academySlug)
ResponseJSON(ctx, gin.H{"academy_slug": academySlug}, res, err)
}
// GetAcademyDetail godoc
// @Summary Get Academy Detail by ID
// @Description Retrieve detailed academy information using its ID
// @Tags Academy
// @Accept json
// @Produce json
// @Param id path string true "Academy ID"
// @Success 200 {object} dto.SuccessResponse[entity.Academy]
// @Failure 400 {object} dto.ErrorResponse
func (c *academyController) GetAcademyDetail(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
res, err := c.academyService.GetAcademyDetail(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, res, err)
}
// ListAcademy godoc
// @Summary List Academies
// @Description Retrieve a paginated list of academies with optional filters
// @Tags Academy
// @Accept json
// @Produce json
// @Param limit query int false "Number of items per page" default(10)
// @Param page query int false "Page number" default(1)
// @Param search query string false "Search term for academy title/name"
// @Param sortBy query string false "Field to sort by"
// @Param order query string false "Sort order (asc or desc)"
// @Param registerStatus query int false "Filter by registration status"
// @Param status query string false "Filter by academy status"
// @Success 200 {object} dto.SuccessResponse[[]entity.Academy]
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/academy [get]
func (c *academyController) ListAcademy(ctx *gin.Context) {
accountId := ParseAccountId(ctx)
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
search := ctx.DefaultQuery("search", "")
sortBy := ctx.DefaultQuery("sortBy", "")
order := ctx.DefaultQuery("order", "")
var registerStatus *int
if val := ctx.Query("registerStatus"); val != "" {
if i, err := strconv.Atoi(val); err == nil {
registerStatus = &i
}
}
isModified := false
if limit < 1 {
limit = 10
isModified = true
} else if limit > 50 {
limit = 50
isModified = true
}
if page < 1 {
page = 1
isModified = true
}
var status *string
if val := ctx.Query("status"); val != "" {
if val == entity.StatusNotStarted || val == entity.StatusInProgress || val == entity.StatusFinished {
status = &val
}
}
offset := (page - 1) * limit
p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order, RegisterStatus: registerStatus, Status: status}
list, total, err := c.academyService.ListAcademy(ctx.Request.Context(), accountId, p)
if err != nil {
ResponseJSON[any, any](ctx, nil, nil, err)
return
}
var totalPages int
if total == 0 {
totalPages = 1
} else {
totalPages = int((total + int64(limit) - 1) / int64(limit))
}
if page > totalPages {
page = totalPages
offset = (page - 1) * limit
p.Offset = offset
list, total, err = c.academyService.ListAcademy(ctx.Request.Context(), accountId, p)
isModified = true
}
meta := gin.H{
"totalItems": total,
"totalPages": totalPages,
"currentPage": page,
}
if isModified {
ctx.Status(http.StatusAccepted)
}
ResponseJSON(ctx, meta, list, err)
}
// CreateAcademy godoc
// @Summary Create Academy
// @Description Create a new academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param request body dto.CreateAcademyRequest true "Create Academy Request"
// @Success 200 {object} dto.SuccessResponse[entity.Academy]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) CreateAcademy(ctx *gin.Context) {
req := RequestJSON[dto.CreateAcademyRequest](ctx)
res, err := c.academyService.CreateAcademy(ctx.Request.Context(), req)
ResponseJSON(ctx, req, res, err)
}
// UpdateAcademy godoc
// @Summary Update Academy
// @Description Update an existing academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param id path string true "Academy ID"
// @Param request body dto.UpdateAcademyRequest true "Update Academy Request"
// @Success 200 {object} dto.SuccessResponse[entity.Academy]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) UpdateAcademy(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
req := RequestJSON[dto.UpdateAcademyRequest](ctx)
res, err := c.academyService.UpdateAcademy(ctx.Request.Context(), id, req)
ResponseJSON(ctx, req, res, err)
}
// DeleteAcademy godoc
// @Summary Delete Academy
// @Description Delete an existing academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param id path string true "Academy ID"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) DeleteAcademy(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
err := c.academyService.DeleteAcademy(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
}
// ================= MATERIAL =================
// GetMaterial godoc
// @Summary Get Material by Slug
// @Description Retrieve material details using its slug
// @Tags Material
// @Accept json
// @Produce json
// @Param academy_slug path string true "Academy Slug"
// @Param material_slug path string true "Material Slug"
// @Success 200 {object} dto.SuccessResponse[dto.MaterialDetailResponse]
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/academy/{academy_slug}/{material_slug} [get]
func (c *academyController) GetMaterial(ctx *gin.Context) {
academySlug := ctx.Param("academy_slug")
materialSlug := ctx.Param("material_slug")
accountId := ParseAccountId(ctx)
res, err := c.academyService.GetMaterialResponse(ctx.Request.Context(), accountId, academySlug, materialSlug)
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug}, res, err)
}
// ListMaterialsByAcademy godoc
// @Summary List Materials by Academy
// @Description Retrieve a list of materials for a specific academy
// @Tags Material
// @Accept json
// @Produce json
// @Param id path string true "Academy ID"
// @Success 200 {object} dto.SuccessResponse[[]entity.AcademyMaterial]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) ListMaterialsByAcademy(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
res, err := c.academyService.ListMaterialsByAcademy(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"academy_id": id}, res, err)
}
// GetMaterialDetail godoc
// @Summary Get Material Detail by ID
// @Description Retrieve detailed material information using its ID
// @Tags Material
// @Accept json
// @Produce json
// @Param id path string true "Material ID"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyMaterial]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) GetMaterialDetail(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
res, err := c.academyService.GetMaterialDetail(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, res, err)
}
// CreateMaterial godoc
// @Summary Create Material
// @Description Create a new material for an academy
// @Tags Material
// @Accept json
// @Produce json
// @Param request body dto.CreateMaterialRequest true "Create Material Request"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyMaterial]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) CreateMaterial(ctx *gin.Context) {
req := RequestJSON[dto.CreateMaterialRequest](ctx)
res, err := c.academyService.CreateMaterial(ctx.Request.Context(), req)
ResponseJSON(ctx, req, res, err)
}
// UpdateMaterial godoc
// @Summary Update Material
// @Description Update an existing material
// @Tags Material
// @Accept json
// @Produce json
// @Param id path string true "Material ID"
// @Param request body dto.UpdateMaterialRequest true "Update Material Request"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyMaterial]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) UpdateMaterial(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
req := RequestJSON[dto.UpdateMaterialRequest](ctx)
res, err := c.academyService.UpdateMaterial(ctx.Request.Context(), id, req)
ResponseJSON(ctx, req, res, err)
}
// DeleteMaterial godoc
// @Summary Delete Material
// @Description Delete an existing material
// @Tags Material
// @Accept json
// @Produce json
// @Param id path string true "Material ID"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) DeleteMaterial(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
err := c.academyService.DeleteMaterial(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
}
// ================= CONTENT =================
// GetContent godoc
// @Summary Get Content by Order
// @Description Retrieve content details using its order within a material
// @Tags Content
// @Accept json
// @Produce json
// @Param academy_slug path string true "Academy Slug"
// @Param material_slug path string true "Material Slug"
// @Param order path int true "Content Order"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/academy/{academy_slug}/{material_slug}/{order} [get]
func (c *academyController) GetContent(ctx *gin.Context) {
accountId := ParseAccountId(ctx)
academySlug := ctx.Param("academy_slug")
materialSlug := ctx.Param("material_slug")
orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
return
}
order := uint(orderID64)
res, err := c.academyService.GetContent(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
}
// ListContentsByMaterial godoc
// @Summary List Contents by Academy
// @Description Retrieve a paginated list of all contents in a specific academy
// @Tags Content
// @Accept json
// @Produce json
// @Param academy_slug path string true "Academy Slug"
// @Param limit query int false "Items per page" default(10)
// @Param page query int false "Page number" default(1)
// @Param search query string false "Search by content title/body or material title"
// @Param sortBy query string false "Sort field (title, order, created_at, material_title)"
// @Param orderBy query string false "Sort direction (asc / desc)"
// @Success 200 {object} dto.SuccessResponse[[]entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/academy/{academy_slug}/contents [get]
func (c *academyController) ListContentsByAcademy(ctx *gin.Context) {
academySlug := ctx.Param("academy_slug")
accountId := ParseAccountId(ctx)
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
search := ctx.DefaultQuery("search", "")
sortBy := ctx.DefaultQuery("sortBy", "")
order := ctx.DefaultQuery("orderBy", "")
if order == "" {
order = ctx.DefaultQuery("order", "")
}
if limit < 1 {
limit = 10
} else if limit > 100 {
limit = 100
}
if page < 1 {
page = 1
}
offset := (page - 1) * limit
p := entity.Pagination{Limit: limit, Offset: offset, Search: search, SortBy: sortBy, Order: order}
list, total, err := c.academyService.ListContentsByAcademy(ctx.Request.Context(), accountId, academySlug, p)
if err != nil {
ResponseJSON[any, any](ctx, nil, nil, err)
return
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
if total == 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
}
meta := gin.H{
"academy_slug": academySlug,
"totalItems": total,
"totalPages": totalPages,
"currentPage": page,
"limit": limit,
}
ResponseJSON(ctx, meta, list, nil)
}
// ListContentsByMaterial godoc
// @Summary List Contents by Material
// @Description Retrieve a list of contents for a specific material
// @Tags Content
// @Accept json
// @Produce json
// @Param id path string true "Material ID"
// @Success 200 {object} dto.SuccessResponse[[]entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) ListContentsByMaterial(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
res, err := c.academyService.ListContentsByMaterial(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"material_id": id}, res, err)
}
// GetContentDetail godoc
// @Summary Get Content Detail by ID
// @Description Retrieve detailed content information using its ID
// @Tags Content
// @Accept json
// @Produce json
// @Param id path string true "Content ID"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) GetContentDetail(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
res, err := c.academyService.GetContentDetail(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, res, err)
}
// CreateContent godoc
// @Summary Create Content
// @Description Create a new content for a material
// @Tags Content
// @Accept json
// @Produce json
// @Param request body dto.CreateContentRequest true "Create Content Request"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) CreateContent(ctx *gin.Context) {
req := RequestJSON[dto.CreateContentRequest](ctx)
res, err := c.academyService.CreateContent(ctx.Request.Context(), req)
ResponseJSON(ctx, req, res, err)
}
// UpdateContent godoc
// @Summary Update Content
// @Description Update an existing content
// @Tags Content
// @Accept json
// @Produce json
// @Param id path string true "Content ID"
// @Param request body dto.UpdateContentRequest true "Update Content Request"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyContent]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) UpdateContent(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
req := RequestJSON[dto.UpdateContentRequest](ctx)
res, err := c.academyService.UpdateContent(ctx.Request.Context(), id, req)
ResponseJSON(ctx, req, res, err)
}
// DeleteContent godoc
// @Summary Delete Content
// @Description Delete an existing content
// @Tags Content
// @Accept json
// @Produce json
// @Param id path string true "Content ID"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) DeleteContent(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
err := c.academyService.DeleteContent(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
}
// ================= PROGRESS =================
// UpdateContentProgress godoc
// @Summary Update Content Progress
// @Description Update the progress of a content within a material
// @Tags Progress
// @Accept json
// @Produce json
// @Param academy_slug path string true "Academy Slug"
// @Param material_slug path string true "Material Slug"
// @Param order path int true "Content Order"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
// @Router /api/v1/academy/{academy_slug}/{material_slug}/{order} [post]
func (c *academyController) UpdateContentProgress(ctx *gin.Context) {
accountId := ParseAccountId(ctx)
academySlug := ctx.Param("academy_slug")
materialSlug := ctx.Param("material_slug")
orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
return
}
order := uint(orderID64)
contentProgress, materialProgress, academyProgress, err := c.academyService.UpdateContentProgress(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
res := gin.H{
"content_progress": contentProgress,
"material_progress": materialProgress,
"academy_progress": academyProgress,
}
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
}
// JoinAcademyByCode godoc
// @Summary Join Academy by Code
// @Description Join an academy using a unique code
// @Tags Academy
// @Accept json
// @Produce json
// @Param request body dto.JoinAcademyByCodeRequest true "Join Academy by Code Request"
// @Success 200 {object} dto.SuccessResponse[dto.AcademyMiniDetailResponse]
// @Failure 400 {object} dto.ErrorResponse
// @Failure 402 {object} dto.RegisterAcademyPaymentActionRequiredResponse
// @Security BearerAuth
// @Router /api/v1/academy/join [post]
func (c *academyController) JoinAcademyByCode(ctx *gin.Context) {
req := RequestJSON[dto.JoinAcademyByCodeRequest](ctx)
if err := utils.ValidateCode(req.Code); err != nil {
ResponseJSON[any, any](ctx, req, nil, http_error.INVALID_CODE)
return
}
accountId := ParseAccountId(ctx)
res, err := c.academyService.JoinByCode(ctx.Request.Context(), accountId, req.Code)
if errors.Is(err, http_error.PAYMENT_REQUIRED) {
ctx.JSON(http.StatusPaymentRequired, dto.RegisterAcademyPaymentActionRequiredResponse{
Data: dto.RegisterAcademyPaymentActionRequiredData{
AccountID: res.Payment.AccountId.String(),
Amount: res.Payment.Amount,
AcademyID: res.Payment.AcademyId.String(),
ExpiredAt: res.Payment.ExpiredAt.Format(time.RFC3339),
ID: res.Payment.Id.String(),
InvoiceID: res.Payment.InvoiceId,
InvoiceURL: res.Payment.InvoiceUrl,
Status: res.Payment.Status,
TransactionAt: res.Payment.TransactionAt.Format(time.RFC3339),
MayarTransactionID: res.Payment.ExternalId,
},
Message: http_error.PAYMENT_REQUIRED.Error(),
MetaData: req.Code,
Status: "action_required",
})
return
}
ResponseJSON(ctx, req, res, err)
}
// AssignAccountToAcademy godoc
// @Summary Assign Account to Academy
// @Description Assign an account to an academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param request body dto.AssignRequest true "Assign Account to Academy Request"
// @Success 200 {object} dto.SuccessResponse[entity.AcademyAssign]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) AssignAccountToAcademy(ctx *gin.Context) {
req := RequestJSON[dto.AssignRequest](ctx)
academyId, errA := uuid.Parse(req.AcademyId)
accountId, errB := uuid.Parse(req.AccountId)
if errA != nil || errB != nil {
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
return
}
res, err := c.academyService.AssignAccountToAcademy(ctx.Request.Context(), academyId, accountId)
ResponseJSON(ctx, req, res, err)
}
// UnassignAccountFromAcademy godoc
// @Summary Unassign Account from Academy
// @Description Unassign an account from an academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param id path string true "Assignment ID"
// @Success 200 {object} dto.SuccessResponse[any]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) UnassignAccountFromAcademy(ctx *gin.Context) {
id := ParseUUID(ctx, "id")
err := c.academyService.UnassignAccountFromAcademy(ctx.Request.Context(), id)
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
}
// ListAssignmentsByAcademy godoc
// @Summary List Assignments by Academy
// @Description Retrieve a list of assignments for a specific academy
// @Tags Academy
// @Accept json
// @Produce json
// @Param academy_id path string true "Academy ID"
// @Success 200 {object} dto.SuccessResponse[[]entity.AcademyAssign]
// @Failure 400 {object} dto.ErrorResponse
// @Security BearerAuth
func (c *academyController) ListAssignmentsByAcademy(ctx *gin.Context) {
academyId := ParseUUID(ctx, "academy_id")
res, err := c.academyService.ListAssignmentsByAcademy(ctx.Request.Context(), academyId)
ResponseJSON(ctx, gin.H{"academy_id": academyId}, res, err)
}