Yimaru-BackEnd/internal/services/appversions/service.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

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
}