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