Introduce admin CRUD and public version check APIs for Play Store/App Store releases with force or optional update policies, and update profile dropdown seed data for countries, regions, and learner profile fields. Co-authored-by: Cursor <cursoragent@cursor.com>
391 lines
13 KiB
Go
391 lines
13 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type createMobileAppVersionReq struct {
|
|
Platform string `json:"platform" validate:"required"`
|
|
VersionName string `json:"version_name" validate:"required"`
|
|
VersionCode int32 `json:"version_code" validate:"required,min=1"`
|
|
UpdateType *string `json:"update_type"`
|
|
ReleaseNotes *string `json:"release_notes"`
|
|
StoreURL *string `json:"store_url"`
|
|
MinSupportedVersionCode *int32 `json:"min_supported_version_code"`
|
|
Status *string `json:"status"`
|
|
}
|
|
|
|
type updateMobileAppVersionReq struct {
|
|
VersionName *string `json:"version_name"`
|
|
VersionCode *int32 `json:"version_code"`
|
|
UpdateType *string `json:"update_type"`
|
|
ReleaseNotes *string `json:"release_notes"`
|
|
StoreURL *string `json:"store_url"`
|
|
MinSupportedVersionCode *int32 `json:"min_supported_version_code"`
|
|
Status *string `json:"status"`
|
|
}
|
|
|
|
type mobileAppVersionRes struct {
|
|
ID int64 `json:"id"`
|
|
Platform string `json:"platform"`
|
|
VersionName string `json:"version_name"`
|
|
VersionCode int32 `json:"version_code"`
|
|
UpdateType string `json:"update_type"`
|
|
ReleaseNotes *string `json:"release_notes,omitempty"`
|
|
StoreURL *string `json:"store_url,omitempty"`
|
|
MinSupportedVersionCode *int32 `json:"min_supported_version_code,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt *string `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
type listMobileAppVersionsRes struct {
|
|
Versions []mobileAppVersionRes `json:"versions"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
func mapMobileAppVersionToRes(v domain.MobileAppVersion) mobileAppVersionRes {
|
|
var updatedAt *string
|
|
if v.UpdatedAt != nil {
|
|
value := v.UpdatedAt.String()
|
|
updatedAt = &value
|
|
}
|
|
return mobileAppVersionRes{
|
|
ID: v.ID,
|
|
Platform: v.Platform,
|
|
VersionName: v.VersionName,
|
|
VersionCode: v.VersionCode,
|
|
UpdateType: v.UpdateType,
|
|
ReleaseNotes: v.ReleaseNotes,
|
|
StoreURL: v.StoreURL,
|
|
MinSupportedVersionCode: v.MinSupportedVersionCode,
|
|
Status: v.Status,
|
|
CreatedAt: v.CreatedAt.String(),
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
}
|
|
|
|
// CheckMobileAppVersion godoc
|
|
// @Summary Check mobile app version
|
|
// @Description Public endpoint for mobile clients to determine if an app update is available (force or optional)
|
|
// @Tags app-versions
|
|
// @Produce json
|
|
// @Param platform query string true "Platform: ANDROID or IOS"
|
|
// @Param version_code query int true "Client build number (Android versionCode / iOS build number)"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/app/version/check [get]
|
|
func (h *Handler) CheckMobileAppVersion(c *fiber.Ctx) error {
|
|
platform := strings.TrimSpace(c.Query("platform"))
|
|
if platform == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "platform is required",
|
|
Error: "MISSING_PLATFORM",
|
|
})
|
|
}
|
|
|
|
versionCodeStr := strings.TrimSpace(c.Query("version_code"))
|
|
if versionCodeStr == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "version_code is required",
|
|
Error: "MISSING_VERSION_CODE",
|
|
})
|
|
}
|
|
versionCode, err := strconv.ParseInt(versionCodeStr, 10, 32)
|
|
if err != nil || versionCode <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "version_code must be a positive integer",
|
|
Error: "INVALID_VERSION_CODE",
|
|
})
|
|
}
|
|
|
|
result, err := h.appVersionSvc.CheckMobileAppVersion(c.Context(), platform, int32(versionCode))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to check app version",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "App version check completed",
|
|
Data: result,
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// ListMobileAppVersionsAdmin godoc
|
|
// @Summary List mobile app versions (admin)
|
|
// @Tags app-versions
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param platform query string false "Filter by ANDROID or IOS"
|
|
// @Param status query string false "Filter by ACTIVE or INACTIVE"
|
|
// @Param limit query int false "Limit (default 20)"
|
|
// @Param offset query int false "Offset (default 0)"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/admin/app-versions [get]
|
|
func (h *Handler) ListMobileAppVersionsAdmin(c *fiber.Ctx) error {
|
|
var platformPtr *string
|
|
if platform := strings.TrimSpace(c.Query("platform")); platform != "" {
|
|
platformPtr = &platform
|
|
}
|
|
var statusPtr *string
|
|
if status := strings.TrimSpace(c.Query("status")); status != "" {
|
|
statusPtr = &status
|
|
}
|
|
|
|
limit, err := strconv.Atoi(c.Query("limit", "20"))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid limit",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
offset, err := strconv.Atoi(c.Query("offset", "0"))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid offset",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
versions, total, err := h.appVersionSvc.ListMobileAppVersions(c.Context(), platformPtr, statusPtr, int32(limit), int32(offset))
|
|
if err != nil {
|
|
code := fiber.StatusInternalServerError
|
|
if strings.Contains(err.Error(), "must be one of") {
|
|
code = fiber.StatusBadRequest
|
|
}
|
|
return c.Status(code).JSON(domain.ErrorResponse{
|
|
Message: "Failed to list app versions",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
out := make([]mobileAppVersionRes, 0, len(versions))
|
|
for _, v := range versions {
|
|
out = append(out, mapMobileAppVersionToRes(v))
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "App versions retrieved successfully",
|
|
Data: listMobileAppVersionsRes{
|
|
Versions: out,
|
|
TotalCount: total,
|
|
},
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// GetMobileAppVersionByIDAdmin godoc
|
|
// @Summary Get mobile app version by ID (admin)
|
|
// @Tags app-versions
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "App version ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/admin/app-versions/{id} [get]
|
|
func (h *Handler) GetMobileAppVersionByIDAdmin(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid app version ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
version, err := h.appVersionSvc.GetMobileAppVersionByID(c.Context(), id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "App version not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get app version",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "App version retrieved successfully",
|
|
Data: mapMobileAppVersionToRes(version),
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// CreateMobileAppVersion godoc
|
|
// @Summary Create mobile app version (admin)
|
|
// @Tags app-versions
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param body body createMobileAppVersionReq true "App version payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/admin/app-versions [post]
|
|
func (h *Handler) CreateMobileAppVersion(c *fiber.Ctx) error {
|
|
var req createMobileAppVersionReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Validation failed",
|
|
Error: firstValidationError(valErrs),
|
|
})
|
|
}
|
|
|
|
version, err := h.appVersionSvc.CreateMobileAppVersion(c.Context(), domain.CreateMobileAppVersionInput{
|
|
Platform: req.Platform,
|
|
VersionName: req.VersionName,
|
|
VersionCode: req.VersionCode,
|
|
UpdateType: req.UpdateType,
|
|
ReleaseNotes: req.ReleaseNotes,
|
|
StoreURL: req.StoreURL,
|
|
MinSupportedVersionCode: req.MinSupportedVersionCode,
|
|
Status: req.Status,
|
|
})
|
|
if err != nil {
|
|
code := fiber.StatusInternalServerError
|
|
if strings.Contains(err.Error(), "required") || strings.Contains(err.Error(), "must be") {
|
|
code = fiber.StatusBadRequest
|
|
}
|
|
return c.Status(code).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create app version",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "App version created successfully",
|
|
Data: mapMobileAppVersionToRes(version),
|
|
Success: true,
|
|
StatusCode: fiber.StatusCreated,
|
|
})
|
|
}
|
|
|
|
// UpdateMobileAppVersion godoc
|
|
// @Summary Update mobile app version (admin)
|
|
// @Tags app-versions
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "App version ID"
|
|
// @Param body body updateMobileAppVersionReq true "App version payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/admin/app-versions/{id} [put]
|
|
func (h *Handler) UpdateMobileAppVersion(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid app version ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req updateMobileAppVersionReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
version, err := h.appVersionSvc.UpdateMobileAppVersion(c.Context(), id, domain.UpdateMobileAppVersionInput{
|
|
VersionName: req.VersionName,
|
|
VersionCode: req.VersionCode,
|
|
UpdateType: req.UpdateType,
|
|
ReleaseNotes: req.ReleaseNotes,
|
|
StoreURL: req.StoreURL,
|
|
MinSupportedVersionCode: req.MinSupportedVersionCode,
|
|
Status: req.Status,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "App version not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
code := fiber.StatusInternalServerError
|
|
if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "must be") || strings.Contains(err.Error(), "cannot") {
|
|
code = fiber.StatusBadRequest
|
|
}
|
|
return c.Status(code).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update app version",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "App version updated successfully",
|
|
Data: mapMobileAppVersionToRes(version),
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// DeleteMobileAppVersion godoc
|
|
// @Summary Delete mobile app version (admin)
|
|
// @Tags app-versions
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "App version ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/admin/app-versions/{id} [delete]
|
|
func (h *Handler) DeleteMobileAppVersion(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid app version ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
if err := h.appVersionSvc.DeleteMobileAppVersion(c.Context(), id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "App version not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to delete app version",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "App version deleted successfully",
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|