Yimaru-BackEnd/internal/web_server/handlers/mobile_app_version_handler.go
Yared Yemane a719c0daca Add mobile app version management and refresh profile field seeds.
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>
2026-05-25 06:52:20 -07:00

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,
})
}