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>
238 lines
7.2 KiB
Go
238 lines
7.2 KiB
Go
package appversions
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type Service struct {
|
|
store ports.MobileAppVersionStore
|
|
}
|
|
|
|
func NewService(store ports.MobileAppVersionStore) *Service {
|
|
return &Service{store: store}
|
|
}
|
|
|
|
func normalizePlatform(raw string) (string, error) {
|
|
value := strings.ToUpper(strings.TrimSpace(raw))
|
|
switch value {
|
|
case domain.MobileAppPlatformAndroid, domain.MobileAppPlatformIOS:
|
|
return value, nil
|
|
default:
|
|
return "", fmt.Errorf("platform must be one of %s, %s", domain.MobileAppPlatformAndroid, domain.MobileAppPlatformIOS)
|
|
}
|
|
}
|
|
|
|
func normalizeUpdateType(raw *string) (string, error) {
|
|
if raw == nil || strings.TrimSpace(*raw) == "" {
|
|
return domain.MobileAppUpdateTypeOptional, nil
|
|
}
|
|
value := strings.ToUpper(strings.TrimSpace(*raw))
|
|
switch value {
|
|
case domain.MobileAppUpdateTypeForce, domain.MobileAppUpdateTypeOptional:
|
|
return value, nil
|
|
default:
|
|
return "", fmt.Errorf("update_type must be one of %s, %s", domain.MobileAppUpdateTypeForce, domain.MobileAppUpdateTypeOptional)
|
|
}
|
|
}
|
|
|
|
func normalizeStatus(raw *string) (string, error) {
|
|
if raw == nil || strings.TrimSpace(*raw) == "" {
|
|
return domain.MobileAppVersionStatusActive, nil
|
|
}
|
|
value := strings.ToUpper(strings.TrimSpace(*raw))
|
|
switch value {
|
|
case domain.MobileAppVersionStatusActive, domain.MobileAppVersionStatusInactive:
|
|
return value, nil
|
|
default:
|
|
return "", fmt.Errorf("status must be one of %s, %s", domain.MobileAppVersionStatusActive, domain.MobileAppVersionStatusInactive)
|
|
}
|
|
}
|
|
|
|
func optionalTrimmedText(raw *string) *string {
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(*raw)
|
|
if trimmed == "" {
|
|
empty := ""
|
|
return &empty
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func (s *Service) CreateMobileAppVersion(ctx context.Context, input domain.CreateMobileAppVersionInput) (domain.MobileAppVersion, error) {
|
|
platform, err := normalizePlatform(input.Platform)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
input.Platform = platform
|
|
|
|
input.VersionName = strings.TrimSpace(input.VersionName)
|
|
if input.VersionName == "" {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("version_name is required")
|
|
}
|
|
if input.VersionCode <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("version_code must be a positive integer")
|
|
}
|
|
if input.MinSupportedVersionCode != nil && *input.MinSupportedVersionCode <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("min_supported_version_code must be a positive integer")
|
|
}
|
|
if input.MinSupportedVersionCode != nil && *input.MinSupportedVersionCode > input.VersionCode {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("min_supported_version_code cannot exceed version_code")
|
|
}
|
|
|
|
updateType, err := normalizeUpdateType(input.UpdateType)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
input.UpdateType = &updateType
|
|
|
|
status, err := normalizeStatus(input.Status)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
input.Status = &status
|
|
|
|
input.ReleaseNotes = optionalTrimmedText(input.ReleaseNotes)
|
|
input.StoreURL = optionalTrimmedText(input.StoreURL)
|
|
|
|
return s.store.CreateMobileAppVersion(ctx, input)
|
|
}
|
|
|
|
func (s *Service) UpdateMobileAppVersion(ctx context.Context, id int64, input domain.UpdateMobileAppVersionInput) (domain.MobileAppVersion, error) {
|
|
if id <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("invalid app version id")
|
|
}
|
|
if input.VersionName != nil {
|
|
trimmed := strings.TrimSpace(*input.VersionName)
|
|
if trimmed == "" {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("version_name cannot be empty")
|
|
}
|
|
input.VersionName = &trimmed
|
|
}
|
|
if input.VersionCode != nil && *input.VersionCode <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("version_code must be a positive integer")
|
|
}
|
|
if input.MinSupportedVersionCode != nil && *input.MinSupportedVersionCode <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("min_supported_version_code must be a positive integer")
|
|
}
|
|
if input.UpdateType != nil {
|
|
updateType, err := normalizeUpdateType(input.UpdateType)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
input.UpdateType = &updateType
|
|
}
|
|
if input.Status != nil {
|
|
status, err := normalizeStatus(input.Status)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
input.Status = &status
|
|
}
|
|
input.ReleaseNotes = optionalTrimmedText(input.ReleaseNotes)
|
|
input.StoreURL = optionalTrimmedText(input.StoreURL)
|
|
|
|
updated, err := s.store.UpdateMobileAppVersion(ctx, id, input)
|
|
if err != nil {
|
|
return domain.MobileAppVersion{}, err
|
|
}
|
|
if updated.MinSupportedVersionCode != nil && *updated.MinSupportedVersionCode > updated.VersionCode {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("min_supported_version_code cannot exceed version_code")
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
func (s *Service) GetMobileAppVersionByID(ctx context.Context, id int64) (domain.MobileAppVersion, error) {
|
|
if id <= 0 {
|
|
return domain.MobileAppVersion{}, fmt.Errorf("invalid app version id")
|
|
}
|
|
return s.store.GetMobileAppVersionByID(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ListMobileAppVersions(ctx context.Context, platform *string, status *string, limit int32, offset int32) ([]domain.MobileAppVersion, int64, error) {
|
|
if platform != nil {
|
|
normalized, err := normalizePlatform(*platform)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
platform = &normalized
|
|
}
|
|
if status != nil {
|
|
normalized, err := normalizeStatus(status)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
status = &normalized
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListMobileAppVersions(ctx, platform, status, limit, offset)
|
|
}
|
|
|
|
func (s *Service) DeleteMobileAppVersion(ctx context.Context, id int64) error {
|
|
if id <= 0 {
|
|
return fmt.Errorf("invalid app version id")
|
|
}
|
|
return s.store.DeleteMobileAppVersion(ctx, id)
|
|
}
|
|
|
|
func (s *Service) CheckMobileAppVersion(ctx context.Context, platform string, clientVersionCode int32) (domain.MobileAppVersionCheckResult, error) {
|
|
normalizedPlatform, err := normalizePlatform(platform)
|
|
if err != nil {
|
|
return domain.MobileAppVersionCheckResult{}, err
|
|
}
|
|
if clientVersionCode <= 0 {
|
|
return domain.MobileAppVersionCheckResult{}, fmt.Errorf("version_code must be a positive integer")
|
|
}
|
|
|
|
latest, err := s.store.GetLatestActiveMobileAppVersion(ctx, normalizedPlatform)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.MobileAppVersionCheckResult{
|
|
Platform: normalizedPlatform,
|
|
ClientVersionCode: clientVersionCode,
|
|
UpdateAvailable: false,
|
|
}, nil
|
|
}
|
|
return domain.MobileAppVersionCheckResult{}, err
|
|
}
|
|
|
|
result := domain.MobileAppVersionCheckResult{
|
|
Platform: normalizedPlatform,
|
|
ClientVersionCode: clientVersionCode,
|
|
LatestVersionName: latest.VersionName,
|
|
LatestVersionCode: latest.VersionCode,
|
|
ReleaseNotes: latest.ReleaseNotes,
|
|
StoreURL: latest.StoreURL,
|
|
}
|
|
|
|
if clientVersionCode >= latest.VersionCode {
|
|
result.UpdateAvailable = false
|
|
return result, nil
|
|
}
|
|
|
|
result.UpdateAvailable = true
|
|
result.UpdateType = latest.UpdateType
|
|
result.ForceUpdate = latest.UpdateType == domain.MobileAppUpdateTypeForce
|
|
if latest.MinSupportedVersionCode != nil && clientVersionCode < *latest.MinSupportedVersionCode {
|
|
result.ForceUpdate = true
|
|
}
|
|
|
|
return result, nil
|
|
}
|