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>
This commit is contained in:
parent
3f73afb4bf
commit
a719c0daca
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"Yimaru-Backend/internal/repository"
|
"Yimaru-Backend/internal/repository"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
|
"Yimaru-Backend/internal/services/appversions"
|
||||||
"Yimaru-Backend/internal/services/chapa"
|
"Yimaru-Backend/internal/services/chapa"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
|
|
@ -400,6 +401,7 @@ func main() {
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
faqSvc := faqs.NewService(repository.NewFAQStore(store))
|
faqSvc := faqs.NewService(repository.NewFAQStore(store))
|
||||||
|
appVersionSvc := appversions.NewService(repository.NewMobileAppVersionStore(store))
|
||||||
personasSvc := personasservice.NewService(store)
|
personasSvc := personasservice.NewService(store)
|
||||||
examPrepSvc := examprep.NewService(store)
|
examPrepSvc := examprep.NewService(store)
|
||||||
|
|
||||||
|
|
@ -480,6 +482,7 @@ func main() {
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
faqSvc,
|
faqSvc,
|
||||||
|
appVersionSvc,
|
||||||
emailTemplateSvc,
|
emailTemplateSvc,
|
||||||
profileFieldOptionSvc,
|
profileFieldOptionSvc,
|
||||||
personasSvc,
|
personasSvc,
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@ INSERT INTO field_options (field_key, code, label, display_order, status) VALUES
|
||||||
('education_level', 'DOCTORATE', 'Doctorate', 8, 'ACTIVE'),
|
('education_level', 'DOCTORATE', 'Doctorate', 8, 'ACTIVE'),
|
||||||
('education_level', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('education_level', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||||
|
|
||||||
('occupation', 'STUDENT', 'Student', 1, 'ACTIVE'),
|
('occupation', 'STUDENTS', 'Students (High school & University)', 1, 'ACTIVE'),
|
||||||
('occupation', 'EMPLOYED', 'Employed', 2, 'ACTIVE'),
|
('occupation', 'JOB_SEEKERS', 'Job Seekers / Fresh Graduates', 2, 'ACTIVE'),
|
||||||
('occupation', 'SELF_EMPLOYED', 'Self-employed', 3, 'ACTIVE'),
|
('occupation', 'WORKING_PROFESSIONALS', 'Working Professionals (Corporate/Office)', 3, 'ACTIVE'),
|
||||||
('occupation', 'UNEMPLOYED', 'Unemployed', 4, 'ACTIVE'),
|
('occupation', 'GOVERNMENT_NGO', 'Government & NGO Workers', 4, 'ACTIVE'),
|
||||||
('occupation', 'HOMEMAKER', 'Homemaker', 5, 'ACTIVE'),
|
('occupation', 'ENTREPRENEURS', 'Entrepreneurs & Small Business Owners', 5, 'ACTIVE'),
|
||||||
('occupation', 'RETIRED', 'Retired', 6, 'ACTIVE'),
|
('occupation', 'HOSPITALITY_TOURISM', 'Hospitality & Tourism Workers', 6, 'ACTIVE'),
|
||||||
('occupation', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('occupation', 'FREELANCERS_REMOTE', 'Freelancers / Remote Workers (Digital Economy)', 7, 'ACTIVE'),
|
||||||
|
|
||||||
('age_group', 'UNDER_13', 'Under 13', 1, 'ACTIVE'),
|
('age_group', 'UNDER_13', 'Under 13', 1, 'ACTIVE'),
|
||||||
('age_group', '13_17', '13–17', 2, 'ACTIVE'),
|
('age_group', '13_17', '13–17', 2, 'ACTIVE'),
|
||||||
|
|
@ -51,64 +51,186 @@ INSERT INTO field_options (field_key, code, label, display_order, status) VALUES
|
||||||
('learning_goal', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('learning_goal', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||||
|
|
||||||
('language_challange', 'PRONUNCIATION', 'Pronunciation', 1, 'ACTIVE'),
|
('language_challange', 'PRONUNCIATION', 'Pronunciation', 1, 'ACTIVE'),
|
||||||
('language_challange', 'GRAMMAR', 'Grammar', 2, 'ACTIVE'),
|
('language_challange', 'WORDS_GRAMMAR', 'Finding words or grammar quickly', 2, 'ACTIVE'),
|
||||||
('language_challange', 'VOCABULARY', 'Vocabulary', 3, 'ACTIVE'),
|
('language_challange', 'CONFIDENCE', 'Feeling nervous or lacking confidence', 3, 'ACTIVE'),
|
||||||
('language_challange', 'LISTENING', 'Listening', 4, 'ACTIVE'),
|
('language_challange', 'ACCENTS_FAST_SPEECH', 'Understanding accents or fast speech', 4, 'ACTIVE'),
|
||||||
('language_challange', 'SPEAKING', 'Speaking confidence', 5, 'ACTIVE'),
|
|
||||||
('language_challange', 'WRITING', 'Writing', 6, 'ACTIVE'),
|
|
||||||
('language_challange', 'READING', 'Reading', 7, 'ACTIVE'),
|
|
||||||
('language_challange', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('language_challange', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||||
|
|
||||||
('language_goal', 'BASIC', 'Basic communication', 1, 'ACTIVE'),
|
('language_goal', 'SPEAK_CONFIDENTLY', 'Speak confidently at work or school', 1, 'ACTIVE'),
|
||||||
('language_goal', 'CONVERSATIONAL', 'Conversational fluency', 2, 'ACTIVE'),
|
('language_goal', 'TRAVEL_DAILY', 'Travel or handle daily situations', 2, 'ACTIVE'),
|
||||||
('language_goal', 'PROFESSIONAL', 'Professional proficiency', 3, 'ACTIVE'),
|
('language_goal', 'FAMILY_FRIENDS', 'Connect with family or friends', 3, 'ACTIVE'),
|
||||||
('language_goal', 'ACADEMIC', 'Academic proficiency', 4, 'ACTIVE'),
|
('language_goal', 'GENERAL_SKILLS', 'General skills expansion', 4, 'ACTIVE'),
|
||||||
('language_goal', 'NATIVE_LIKE', 'Near-native fluency', 5, 'ACTIVE'),
|
('language_goal', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||||
|
|
||||||
('favourite_topic', 'BUSINESS', 'Business', 1, 'ACTIVE'),
|
('favourite_topic', 'FOOD_COOKING', 'Food & Cooking', 1, 'ACTIVE'),
|
||||||
('favourite_topic', 'TECHNOLOGY', 'Technology', 2, 'ACTIVE'),
|
('favourite_topic', 'HOBBIES_SPORTS_MUSIC', 'Hobbies, Sports, Music', 2, 'ACTIVE'),
|
||||||
('favourite_topic', 'HEALTH', 'Health', 3, 'ACTIVE'),
|
('favourite_topic', 'TECH_NEWS_BUSINESS', 'Tech, News, Business', 3, 'ACTIVE'),
|
||||||
('favourite_topic', 'CULTURE', 'Culture', 4, 'ACTIVE'),
|
('favourite_topic', 'TRAVEL_PLACES_CULTURE', 'Travel, Places, Culture', 4, 'ACTIVE'),
|
||||||
('favourite_topic', 'TRAVEL', 'Travel', 5, 'ACTIVE'),
|
|
||||||
('favourite_topic', 'ENTERTAINMENT', 'Entertainment', 6, 'ACTIVE'),
|
|
||||||
('favourite_topic', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('favourite_topic', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||||
|
|
||||||
('country', 'ET', 'Ethiopia', 1, 'ACTIVE'),
|
('country', 'AF', 'Afghanistan', 1, 'ACTIVE'),
|
||||||
('country', 'ER', 'Eritrea', 2, 'ACTIVE'),
|
('country', 'AL', 'Albania', 2, 'ACTIVE'),
|
||||||
('country', 'DJ', 'Djibouti', 3, 'ACTIVE'),
|
('country', 'DZ', 'Algeria', 3, 'ACTIVE'),
|
||||||
('country', 'SO', 'Somalia', 4, 'ACTIVE'),
|
('country', 'AD', 'Andorra', 4, 'ACTIVE'),
|
||||||
('country', 'KE', 'Kenya', 5, 'ACTIVE'),
|
('country', 'AO', 'Angola', 5, 'ACTIVE'),
|
||||||
('country', 'SD', 'Sudan', 6, 'ACTIVE'),
|
('country', 'AR', 'Argentina', 6, 'ACTIVE'),
|
||||||
('country', 'SS', 'South Sudan', 7, 'ACTIVE'),
|
('country', 'AM', 'Armenia', 7, 'ACTIVE'),
|
||||||
('country', 'UG', 'Uganda', 8, 'ACTIVE'),
|
('country', 'AU', 'Australia', 8, 'ACTIVE'),
|
||||||
('country', 'RW', 'Rwanda', 9, 'ACTIVE'),
|
('country', 'AT', 'Austria', 9, 'ACTIVE'),
|
||||||
('country', 'TZ', 'Tanzania', 10, 'ACTIVE'),
|
('country', 'AZ', 'Azerbaijan', 10, 'ACTIVE'),
|
||||||
('country', 'EG', 'Egypt', 11, 'ACTIVE'),
|
('country', 'BH', 'Bahrain', 11, 'ACTIVE'),
|
||||||
('country', 'NG', 'Nigeria', 12, 'ACTIVE'),
|
('country', 'BD', 'Bangladesh', 12, 'ACTIVE'),
|
||||||
('country', 'ZA', 'South Africa', 13, 'ACTIVE'),
|
('country', 'BY', 'Belarus', 13, 'ACTIVE'),
|
||||||
('country', 'US', 'United States', 20, 'ACTIVE'),
|
('country', 'BE', 'Belgium', 14, 'ACTIVE'),
|
||||||
('country', 'GB', 'United Kingdom', 21, 'ACTIVE'),
|
('country', 'BZ', 'Belize', 15, 'ACTIVE'),
|
||||||
('country', 'CA', 'Canada', 22, 'ACTIVE'),
|
('country', 'BJ', 'Benin', 16, 'ACTIVE'),
|
||||||
('country', 'DE', 'Germany', 23, 'ACTIVE'),
|
('country', 'BT', 'Bhutan', 17, 'ACTIVE'),
|
||||||
('country', 'FR', 'France', 24, 'ACTIVE'),
|
('country', 'BO', 'Bolivia', 18, 'ACTIVE'),
|
||||||
('country', 'IN', 'India', 25, 'ACTIVE'),
|
('country', 'BA', 'Bosnia and Herzegovina', 19, 'ACTIVE'),
|
||||||
('country', 'CN', 'China', 26, 'ACTIVE'),
|
('country', 'BW', 'Botswana', 20, 'ACTIVE'),
|
||||||
('country', 'SA', 'Saudi Arabia', 27, 'ACTIVE'),
|
('country', 'BR', 'Brazil', 21, 'ACTIVE'),
|
||||||
('country', 'AE', 'United Arab Emirates', 28, 'ACTIVE'),
|
('country', 'BN', 'Brunei', 22, 'ACTIVE'),
|
||||||
('country', 'OTHER', 'Other', 99, 'ACTIVE'),
|
('country', 'BG', 'Bulgaria', 23, 'ACTIVE'),
|
||||||
|
('country', 'BF', 'Burkina Faso', 24, 'ACTIVE'),
|
||||||
|
('country', 'BI', 'Burundi', 25, 'ACTIVE'),
|
||||||
|
('country', 'KH', 'Cambodia', 26, 'ACTIVE'),
|
||||||
|
('country', 'CM', 'Cameroon', 27, 'ACTIVE'),
|
||||||
|
('country', 'CA', 'Canada', 28, 'ACTIVE'),
|
||||||
|
('country', 'TD', 'Chad', 29, 'ACTIVE'),
|
||||||
|
('country', 'CL', 'Chile', 30, 'ACTIVE'),
|
||||||
|
('country', 'CN', 'China', 31, 'ACTIVE'),
|
||||||
|
('country', 'CO', 'Colombia', 32, 'ACTIVE'),
|
||||||
|
('country', 'KM', 'Comoros', 33, 'ACTIVE'),
|
||||||
|
('country', 'CG', 'Congo', 34, 'ACTIVE'),
|
||||||
|
('country', 'CR', 'Costa Rica', 35, 'ACTIVE'),
|
||||||
|
('country', 'HR', 'Croatia', 36, 'ACTIVE'),
|
||||||
|
('country', 'CU', 'Cuba', 37, 'ACTIVE'),
|
||||||
|
('country', 'CY', 'Cyprus', 38, 'ACTIVE'),
|
||||||
|
('country', 'CZ', 'Czech Republic', 39, 'ACTIVE'),
|
||||||
|
('country', 'DK', 'Denmark', 40, 'ACTIVE'),
|
||||||
|
('country', 'DJ', 'Djibouti', 41, 'ACTIVE'),
|
||||||
|
('country', 'DO', 'Dominican Republic', 42, 'ACTIVE'),
|
||||||
|
('country', 'EC', 'Ecuador', 43, 'ACTIVE'),
|
||||||
|
('country', 'EG', 'Egypt', 44, 'ACTIVE'),
|
||||||
|
('country', 'SV', 'El Salvador', 45, 'ACTIVE'),
|
||||||
|
('country', 'ER', 'Eritrea', 46, 'ACTIVE'),
|
||||||
|
('country', 'EE', 'Estonia', 47, 'ACTIVE'),
|
||||||
|
('country', 'SZ', 'Eswatini', 48, 'ACTIVE'),
|
||||||
|
('country', 'ET', 'Ethiopia', 49, 'ACTIVE'),
|
||||||
|
('country', 'FI', 'Finland', 50, 'ACTIVE'),
|
||||||
|
('country', 'FR', 'France', 51, 'ACTIVE'),
|
||||||
|
('country', 'GA', 'Gabon', 52, 'ACTIVE'),
|
||||||
|
('country', 'GM', 'Gambia', 53, 'ACTIVE'),
|
||||||
|
('country', 'GE', 'Georgia', 54, 'ACTIVE'),
|
||||||
|
('country', 'DE', 'Germany', 55, 'ACTIVE'),
|
||||||
|
('country', 'GH', 'Ghana', 56, 'ACTIVE'),
|
||||||
|
('country', 'GR', 'Greece', 57, 'ACTIVE'),
|
||||||
|
('country', 'GT', 'Guatemala', 58, 'ACTIVE'),
|
||||||
|
('country', 'GN', 'Guinea', 59, 'ACTIVE'),
|
||||||
|
('country', 'HT', 'Haiti', 60, 'ACTIVE'),
|
||||||
|
('country', 'HN', 'Honduras', 61, 'ACTIVE'),
|
||||||
|
('country', 'HU', 'Hungary', 62, 'ACTIVE'),
|
||||||
|
('country', 'IS', 'Iceland', 63, 'ACTIVE'),
|
||||||
|
('country', 'IN', 'India', 64, 'ACTIVE'),
|
||||||
|
('country', 'ID', 'Indonesia', 65, 'ACTIVE'),
|
||||||
|
('country', 'IR', 'Iran', 66, 'ACTIVE'),
|
||||||
|
('country', 'IQ', 'Iraq', 67, 'ACTIVE'),
|
||||||
|
('country', 'IE', 'Ireland', 68, 'ACTIVE'),
|
||||||
|
('country', 'IL', 'Israel', 69, 'ACTIVE'),
|
||||||
|
('country', 'IT', 'Italy', 70, 'ACTIVE'),
|
||||||
|
('country', 'JM', 'Jamaica', 71, 'ACTIVE'),
|
||||||
|
('country', 'JP', 'Japan', 72, 'ACTIVE'),
|
||||||
|
('country', 'JO', 'Jordan', 73, 'ACTIVE'),
|
||||||
|
('country', 'KZ', 'Kazakhstan', 74, 'ACTIVE'),
|
||||||
|
('country', 'KE', 'Kenya', 75, 'ACTIVE'),
|
||||||
|
('country', 'KW', 'Kuwait', 76, 'ACTIVE'),
|
||||||
|
('country', 'KG', 'Kyrgyzstan', 77, 'ACTIVE'),
|
||||||
|
('country', 'LA', 'Laos', 78, 'ACTIVE'),
|
||||||
|
('country', 'LV', 'Latvia', 79, 'ACTIVE'),
|
||||||
|
('country', 'LB', 'Lebanon', 80, 'ACTIVE'),
|
||||||
|
('country', 'LR', 'Liberia', 81, 'ACTIVE'),
|
||||||
|
('country', 'LY', 'Libya', 82, 'ACTIVE'),
|
||||||
|
('country', 'LT', 'Lithuania', 83, 'ACTIVE'),
|
||||||
|
('country', 'LU', 'Luxembourg', 84, 'ACTIVE'),
|
||||||
|
('country', 'MG', 'Madagascar', 85, 'ACTIVE'),
|
||||||
|
('country', 'MW', 'Malawi', 86, 'ACTIVE'),
|
||||||
|
('country', 'MY', 'Malaysia', 87, 'ACTIVE'),
|
||||||
|
('country', 'MV', 'Maldives', 88, 'ACTIVE'),
|
||||||
|
('country', 'ML', 'Mali', 89, 'ACTIVE'),
|
||||||
|
('country', 'MT', 'Malta', 90, 'ACTIVE'),
|
||||||
|
('country', 'MX', 'Mexico', 91, 'ACTIVE'),
|
||||||
|
('country', 'MD', 'Moldova', 92, 'ACTIVE'),
|
||||||
|
('country', 'MC', 'Monaco', 93, 'ACTIVE'),
|
||||||
|
('country', 'MN', 'Mongolia', 94, 'ACTIVE'),
|
||||||
|
('country', 'MA', 'Morocco', 95, 'ACTIVE'),
|
||||||
|
('country', 'MZ', 'Mozambique', 96, 'ACTIVE'),
|
||||||
|
('country', 'MM', 'Myanmar', 97, 'ACTIVE'),
|
||||||
|
('country', 'NA', 'Namibia', 98, 'ACTIVE'),
|
||||||
|
('country', 'NP', 'Nepal', 99, 'ACTIVE'),
|
||||||
|
('country', 'NL', 'Netherlands', 100, 'ACTIVE'),
|
||||||
|
('country', 'NZ', 'New Zealand', 101, 'ACTIVE'),
|
||||||
|
('country', 'NI', 'Nicaragua', 102, 'ACTIVE'),
|
||||||
|
('country', 'NE', 'Niger', 103, 'ACTIVE'),
|
||||||
|
('country', 'NG', 'Nigeria', 104, 'ACTIVE'),
|
||||||
|
('country', 'KP', 'North Korea', 105, 'ACTIVE'),
|
||||||
|
('country', 'NO', 'Norway', 106, 'ACTIVE'),
|
||||||
|
('country', 'OM', 'Oman', 107, 'ACTIVE'),
|
||||||
|
('country', 'PK', 'Pakistan', 108, 'ACTIVE'),
|
||||||
|
('country', 'PA', 'Panama', 109, 'ACTIVE'),
|
||||||
|
('country', 'PY', 'Paraguay', 110, 'ACTIVE'),
|
||||||
|
('country', 'PE', 'Peru', 111, 'ACTIVE'),
|
||||||
|
('country', 'PH', 'Philippines', 112, 'ACTIVE'),
|
||||||
|
('country', 'PL', 'Poland', 113, 'ACTIVE'),
|
||||||
|
('country', 'PT', 'Portugal', 114, 'ACTIVE'),
|
||||||
|
('country', 'QA', 'Qatar', 115, 'ACTIVE'),
|
||||||
|
('country', 'RO', 'Romania', 116, 'ACTIVE'),
|
||||||
|
('country', 'RU', 'Russia', 117, 'ACTIVE'),
|
||||||
|
('country', 'RW', 'Rwanda', 118, 'ACTIVE'),
|
||||||
|
('country', 'SA', 'Saudi Arabia', 119, 'ACTIVE'),
|
||||||
|
('country', 'SN', 'Senegal', 120, 'ACTIVE'),
|
||||||
|
('country', 'RS', 'Serbia', 121, 'ACTIVE'),
|
||||||
|
('country', 'SG', 'Singapore', 122, 'ACTIVE'),
|
||||||
|
('country', 'SK', 'Slovakia', 123, 'ACTIVE'),
|
||||||
|
('country', 'SI', 'Slovenia', 124, 'ACTIVE'),
|
||||||
|
('country', 'SO', 'Somalia', 125, 'ACTIVE'),
|
||||||
|
('country', 'ZA', 'South Africa', 126, 'ACTIVE'),
|
||||||
|
('country', 'KR', 'South Korea', 127, 'ACTIVE'),
|
||||||
|
('country', 'ES', 'Spain', 128, 'ACTIVE'),
|
||||||
|
('country', 'LK', 'Sri Lanka', 129, 'ACTIVE'),
|
||||||
|
('country', 'SD', 'Sudan', 130, 'ACTIVE'),
|
||||||
|
('country', 'SE', 'Sweden', 131, 'ACTIVE'),
|
||||||
|
('country', 'CH', 'Switzerland', 132, 'ACTIVE'),
|
||||||
|
('country', 'SY', 'Syria', 133, 'ACTIVE'),
|
||||||
|
('country', 'TW', 'Taiwan', 134, 'ACTIVE'),
|
||||||
|
('country', 'TJ', 'Tajikistan', 135, 'ACTIVE'),
|
||||||
|
('country', 'TZ', 'Tanzania', 136, 'ACTIVE'),
|
||||||
|
('country', 'TH', 'Thailand', 137, 'ACTIVE'),
|
||||||
|
('country', 'TN', 'Tunisia', 138, 'ACTIVE'),
|
||||||
|
('country', 'TR', 'Turkey', 139, 'ACTIVE'),
|
||||||
|
('country', 'UG', 'Uganda', 140, 'ACTIVE'),
|
||||||
|
('country', 'UA', 'Ukraine', 141, 'ACTIVE'),
|
||||||
|
('country', 'AE', 'United Arab Emirates', 142, 'ACTIVE'),
|
||||||
|
('country', 'GB', 'United Kingdom', 143, 'ACTIVE'),
|
||||||
|
('country', 'US', 'United States', 144, 'ACTIVE'),
|
||||||
|
('country', 'UY', 'Uruguay', 145, 'ACTIVE'),
|
||||||
|
('country', 'UZ', 'Uzbekistan', 146, 'ACTIVE'),
|
||||||
|
('country', 'VE', 'Venezuela', 147, 'ACTIVE'),
|
||||||
|
('country', 'VN', 'Vietnam', 148, 'ACTIVE'),
|
||||||
|
('country', 'YE', 'Yemen', 149, 'ACTIVE'),
|
||||||
|
('country', 'ZM', 'Zambia', 150, 'ACTIVE'),
|
||||||
|
('country', 'ZW', 'Zimbabwe', 151, 'ACTIVE'),
|
||||||
|
|
||||||
('ethiopia_regions', 'ADDIS_ABABA', 'Addis Ababa', 1, 'ACTIVE'),
|
('ethiopia_regions', 'ADDIS_ABABA', 'Addis Ababa', 1, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 2, 'ACTIVE'),
|
('ethiopia_regions', 'AFAR', 'Afar', 2, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 3, 'ACTIVE'),
|
('ethiopia_regions', 'AMHARA', 'Amhara', 3, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'AFAR', 'Afar', 4, 'ACTIVE'),
|
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 4, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'AMHARA', 'Amhara', 5, 'ACTIVE'),
|
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 5, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'OROMIA', 'Oromia', 6, 'ACTIVE'),
|
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 6, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOMALI', 'Somali', 7, 'ACTIVE'),
|
('ethiopia_regions', 'GAMBELA', 'Gambela', 7, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 8, 'ACTIVE'),
|
('ethiopia_regions', 'HARARI', 'Harari', 8, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 9, 'ACTIVE'),
|
('ethiopia_regions', 'OROMIA', 'Oromia', 9, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'HARARI', 'Harari', 10, 'ACTIVE'),
|
('ethiopia_regions', 'SIDAMA', 'Sidama', 10, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 11, 'ACTIVE'),
|
('ethiopia_regions', 'SOMALI', 'Somali', 11, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA', 'South West Ethiopia', 13, 'ACTIVE'),
|
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA_PEOPLES', 'South West Ethiopia Peoples', 13, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 14, 'ACTIVE'),
|
('ethiopia_regions', 'TIGRAY', 'Tigray', 14, 'ACTIVE');
|
||||||
('ethiopia_regions', 'OTHER', 'Other', 99, 'ACTIVE');
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
INSERT INTO field_options (field_key, code, label, display_order, status) VALUES
|
INSERT INTO field_options (field_key, code, label, display_order, status) VALUES
|
||||||
('ethiopia_regions', 'ADDIS_ABABA', 'Addis Ababa', 1, 'ACTIVE'),
|
('ethiopia_regions', 'ADDIS_ABABA', 'Addis Ababa', 1, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 2, 'ACTIVE'),
|
('ethiopia_regions', 'AFAR', 'Afar', 2, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 3, 'ACTIVE'),
|
('ethiopia_regions', 'AMHARA', 'Amhara', 3, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'AFAR', 'Afar', 4, 'ACTIVE'),
|
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 4, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'AMHARA', 'Amhara', 5, 'ACTIVE'),
|
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 5, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'OROMIA', 'Oromia', 6, 'ACTIVE'),
|
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 6, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOMALI', 'Somali', 7, 'ACTIVE'),
|
('ethiopia_regions', 'GAMBELA', 'Gambela', 7, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 8, 'ACTIVE'),
|
('ethiopia_regions', 'HARARI', 'Harari', 8, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 9, 'ACTIVE'),
|
('ethiopia_regions', 'OROMIA', 'Oromia', 9, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'HARARI', 'Harari', 10, 'ACTIVE'),
|
('ethiopia_regions', 'SIDAMA', 'Sidama', 10, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 11, 'ACTIVE'),
|
('ethiopia_regions', 'SOMALI', 'Somali', 11, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA', 'South West Ethiopia', 13, 'ACTIVE'),
|
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA_PEOPLES', 'South West Ethiopia Peoples', 13, 'ACTIVE'),
|
||||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 14, 'ACTIVE'),
|
('ethiopia_regions', 'TIGRAY', 'Tigray', 14, 'ACTIVE')
|
||||||
('ethiopia_regions', 'OTHER', 'Other', 99, 'ACTIVE')
|
ON CONFLICT (field_key, code) DO UPDATE SET
|
||||||
ON CONFLICT (field_key, code) DO NOTHING;
|
label = EXCLUDED.label,
|
||||||
|
display_order = EXCLUDED.display_order,
|
||||||
|
status = EXCLUDED.status;
|
||||||
|
|
|
||||||
1
db/migrations/000074_mobile_app_versions.down.sql
Normal file
1
db/migrations/000074_mobile_app_versions.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS mobile_app_versions;
|
||||||
19
db/migrations/000074_mobile_app_versions.up.sql
Normal file
19
db/migrations/000074_mobile_app_versions.up.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS mobile_app_versions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
platform VARCHAR(20) NOT NULL CHECK (platform IN ('ANDROID', 'IOS')),
|
||||||
|
version_name VARCHAR(50) NOT NULL,
|
||||||
|
version_code INT NOT NULL CHECK (version_code > 0),
|
||||||
|
update_type VARCHAR(20) NOT NULL DEFAULT 'OPTIONAL' CHECK (update_type IN ('FORCE', 'OPTIONAL')),
|
||||||
|
release_notes TEXT,
|
||||||
|
store_url TEXT,
|
||||||
|
min_supported_version_code INT CHECK (min_supported_version_code IS NULL OR min_supported_version_code > 0),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT mobile_app_versions_platform_version_code UNIQUE (platform, version_code),
|
||||||
|
CONSTRAINT mobile_app_versions_platform_version_name UNIQUE (platform, version_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mobile_app_versions_platform ON mobile_app_versions (platform);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mobile_app_versions_status ON mobile_app_versions (status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mobile_app_versions_platform_code ON mobile_app_versions (platform, version_code DESC);
|
||||||
62
internal/domain/mobile_app_version.go
Normal file
62
internal/domain/mobile_app_version.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
MobileAppPlatformAndroid = "ANDROID"
|
||||||
|
MobileAppPlatformIOS = "IOS"
|
||||||
|
|
||||||
|
MobileAppUpdateTypeForce = "FORCE"
|
||||||
|
MobileAppUpdateTypeOptional = "OPTIONAL"
|
||||||
|
|
||||||
|
MobileAppVersionStatusActive = "ACTIVE"
|
||||||
|
MobileAppVersionStatusInactive = "INACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MobileAppVersion 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 time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMobileAppVersionInput struct {
|
||||||
|
Platform string
|
||||||
|
VersionName string
|
||||||
|
VersionCode int32
|
||||||
|
UpdateType *string
|
||||||
|
ReleaseNotes *string
|
||||||
|
StoreURL *string
|
||||||
|
MinSupportedVersionCode *int32
|
||||||
|
Status *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMobileAppVersionInput struct {
|
||||||
|
VersionName *string
|
||||||
|
VersionCode *int32
|
||||||
|
UpdateType *string
|
||||||
|
ReleaseNotes *string
|
||||||
|
StoreURL *string
|
||||||
|
MinSupportedVersionCode *int32
|
||||||
|
Status *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MobileAppVersionCheckResult is returned to mobile clients checking for updates.
|
||||||
|
type MobileAppVersionCheckResult struct {
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
ClientVersionCode int32 `json:"client_version_code"`
|
||||||
|
LatestVersionName string `json:"latest_version_name"`
|
||||||
|
LatestVersionCode int32 `json:"latest_version_code"`
|
||||||
|
UpdateAvailable bool `json:"update_available"`
|
||||||
|
ForceUpdate bool `json:"force_update"`
|
||||||
|
UpdateType string `json:"update_type,omitempty"`
|
||||||
|
ReleaseNotes *string `json:"release_notes,omitempty"`
|
||||||
|
StoreURL *string `json:"store_url,omitempty"`
|
||||||
|
}
|
||||||
15
internal/ports/mobile_app_version.go
Normal file
15
internal/ports/mobile_app_version.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MobileAppVersionStore interface {
|
||||||
|
CreateMobileAppVersion(ctx context.Context, input domain.CreateMobileAppVersionInput) (domain.MobileAppVersion, error)
|
||||||
|
UpdateMobileAppVersion(ctx context.Context, id int64, input domain.UpdateMobileAppVersionInput) (domain.MobileAppVersion, error)
|
||||||
|
GetMobileAppVersionByID(ctx context.Context, id int64) (domain.MobileAppVersion, error)
|
||||||
|
ListMobileAppVersions(ctx context.Context, platform *string, status *string, limit int32, offset int32) ([]domain.MobileAppVersion, int64, error)
|
||||||
|
DeleteMobileAppVersion(ctx context.Context, id int64) error
|
||||||
|
GetLatestActiveMobileAppVersion(ctx context.Context, platform string) (domain.MobileAppVersion, error)
|
||||||
|
}
|
||||||
228
internal/repository/mobile_app_versions.go
Normal file
228
internal/repository/mobile_app_versions.go
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMobileAppVersionStore(s *Store) ports.MobileAppVersionStore { return s }
|
||||||
|
|
||||||
|
func mobileAppVersionToDomain(
|
||||||
|
id int64,
|
||||||
|
platform string,
|
||||||
|
versionName string,
|
||||||
|
versionCode int32,
|
||||||
|
updateType string,
|
||||||
|
releaseNotes pgtype.Text,
|
||||||
|
storeURL pgtype.Text,
|
||||||
|
minSupported pgtype.Int4,
|
||||||
|
status string,
|
||||||
|
createdAt pgtype.Timestamptz,
|
||||||
|
updatedAt pgtype.Timestamptz,
|
||||||
|
) domain.MobileAppVersion {
|
||||||
|
var minSupportedPtr *int32
|
||||||
|
if minSupported.Valid {
|
||||||
|
v := minSupported.Int32
|
||||||
|
minSupportedPtr = &v
|
||||||
|
}
|
||||||
|
return domain.MobileAppVersion{
|
||||||
|
ID: id,
|
||||||
|
Platform: platform,
|
||||||
|
VersionName: versionName,
|
||||||
|
VersionCode: versionCode,
|
||||||
|
UpdateType: updateType,
|
||||||
|
ReleaseNotes: fromPgText(releaseNotes),
|
||||||
|
StoreURL: fromPgText(storeURL),
|
||||||
|
MinSupportedVersionCode: minSupportedPtr,
|
||||||
|
Status: status,
|
||||||
|
CreatedAt: createdAt.Time,
|
||||||
|
UpdatedAt: timePtr(updatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanMobileAppVersion(row pgx.Row) (domain.MobileAppVersion, error) {
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
platform string
|
||||||
|
versionName string
|
||||||
|
versionCode int32
|
||||||
|
updateType string
|
||||||
|
releaseNotes pgtype.Text
|
||||||
|
storeURL pgtype.Text
|
||||||
|
minSupported pgtype.Int4
|
||||||
|
status string
|
||||||
|
createdAt pgtype.Timestamptz
|
||||||
|
updatedAt pgtype.Timestamptz
|
||||||
|
)
|
||||||
|
if err := row.Scan(&id, &platform, &versionName, &versionCode, &updateType, &releaseNotes, &storeURL, &minSupported, &status, &createdAt, &updatedAt); err != nil {
|
||||||
|
return domain.MobileAppVersion{}, err
|
||||||
|
}
|
||||||
|
return mobileAppVersionToDomain(id, platform, versionName, versionCode, updateType, releaseNotes, storeURL, minSupported, status, createdAt, updatedAt), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobileAppVersionSelectCols = `
|
||||||
|
id, platform, version_name, version_code, update_type, release_notes, store_url,
|
||||||
|
min_supported_version_code, status, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (s *Store) CreateMobileAppVersion(ctx context.Context, input domain.CreateMobileAppVersionInput) (domain.MobileAppVersion, error) {
|
||||||
|
updateType := domain.MobileAppUpdateTypeOptional
|
||||||
|
if input.UpdateType != nil {
|
||||||
|
updateType = *input.UpdateType
|
||||||
|
}
|
||||||
|
status := domain.MobileAppVersionStatusActive
|
||||||
|
if input.Status != nil {
|
||||||
|
status = *input.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
row := s.conn.QueryRow(ctx, `
|
||||||
|
INSERT INTO mobile_app_versions (
|
||||||
|
platform, version_name, version_code, update_type, release_notes, store_url,
|
||||||
|
min_supported_version_code, status
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
RETURNING `+mobileAppVersionSelectCols,
|
||||||
|
input.Platform,
|
||||||
|
input.VersionName,
|
||||||
|
input.VersionCode,
|
||||||
|
updateType,
|
||||||
|
toPgText(input.ReleaseNotes),
|
||||||
|
toPgText(input.StoreURL),
|
||||||
|
toPgInt4(input.MinSupportedVersionCode),
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
return scanMobileAppVersion(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateMobileAppVersion(ctx context.Context, id int64, input domain.UpdateMobileAppVersionInput) (domain.MobileAppVersion, error) {
|
||||||
|
releaseNotesSet := input.ReleaseNotes != nil
|
||||||
|
var releaseNotesValue pgtype.Text
|
||||||
|
if releaseNotesSet {
|
||||||
|
releaseNotesValue = toPgText(input.ReleaseNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeURLSet := input.StoreURL != nil
|
||||||
|
var storeURLValue pgtype.Text
|
||||||
|
if storeURLSet {
|
||||||
|
storeURLValue = toPgText(input.StoreURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
minSupportedSet := input.MinSupportedVersionCode != nil
|
||||||
|
var minSupportedValue pgtype.Int4
|
||||||
|
if minSupportedSet {
|
||||||
|
minSupportedValue = toPgInt4(input.MinSupportedVersionCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := s.conn.QueryRow(ctx, `
|
||||||
|
UPDATE mobile_app_versions
|
||||||
|
SET version_name = COALESCE($2, version_name),
|
||||||
|
version_code = COALESCE($3, version_code),
|
||||||
|
update_type = COALESCE($4, update_type),
|
||||||
|
release_notes = CASE WHEN $5::boolean THEN $6 ELSE release_notes END,
|
||||||
|
store_url = CASE WHEN $7::boolean THEN $8 ELSE store_url END,
|
||||||
|
min_supported_version_code = CASE WHEN $9::boolean THEN $10::int ELSE min_supported_version_code END,
|
||||||
|
status = COALESCE($11, status),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING `+mobileAppVersionSelectCols,
|
||||||
|
id,
|
||||||
|
input.VersionName,
|
||||||
|
input.VersionCode,
|
||||||
|
input.UpdateType,
|
||||||
|
releaseNotesSet,
|
||||||
|
releaseNotesValue,
|
||||||
|
storeURLSet,
|
||||||
|
storeURLValue,
|
||||||
|
minSupportedSet,
|
||||||
|
minSupportedValue,
|
||||||
|
input.Status,
|
||||||
|
)
|
||||||
|
return scanMobileAppVersion(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetMobileAppVersionByID(ctx context.Context, id int64) (domain.MobileAppVersion, error) {
|
||||||
|
row := s.conn.QueryRow(ctx, `
|
||||||
|
SELECT `+mobileAppVersionSelectCols+`
|
||||||
|
FROM mobile_app_versions
|
||||||
|
WHERE id = $1
|
||||||
|
`, id)
|
||||||
|
return scanMobileAppVersion(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListMobileAppVersions(ctx context.Context, platform *string, status *string, limit int32, offset int32) ([]domain.MobileAppVersion, int64, error) {
|
||||||
|
rows, err := s.conn.Query(ctx, `
|
||||||
|
SELECT `+mobileAppVersionSelectCols+`
|
||||||
|
FROM mobile_app_versions
|
||||||
|
WHERE ($1::text IS NULL OR platform = $1)
|
||||||
|
AND ($2::text IS NULL OR status = $2)
|
||||||
|
ORDER BY platform ASC, version_code DESC, id DESC
|
||||||
|
LIMIT $3 OFFSET $4
|
||||||
|
`, toPgText(platform), toPgText(status), limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
versions := make([]domain.MobileAppVersion, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
rowPlatform string
|
||||||
|
versionName string
|
||||||
|
versionCode int32
|
||||||
|
updateType string
|
||||||
|
releaseNotes pgtype.Text
|
||||||
|
storeURL pgtype.Text
|
||||||
|
minSupported pgtype.Int4
|
||||||
|
rowStatus string
|
||||||
|
createdAt pgtype.Timestamptz
|
||||||
|
updatedAt pgtype.Timestamptz
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&id, &rowPlatform, &versionName, &versionCode, &updateType, &releaseNotes, &storeURL, &minSupported, &rowStatus, &createdAt, &updatedAt); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
versions = append(versions, mobileAppVersionToDomain(id, rowPlatform, versionName, versionCode, updateType, releaseNotes, storeURL, minSupported, rowStatus, createdAt, updatedAt))
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount int64
|
||||||
|
if err := s.conn.QueryRow(ctx, `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM mobile_app_versions
|
||||||
|
WHERE ($1::text IS NULL OR platform = $1)
|
||||||
|
AND ($2::text IS NULL OR status = $2)
|
||||||
|
`, toPgText(platform), toPgText(status)).Scan(&totalCount); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions, totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteMobileAppVersion(ctx context.Context, id int64) error {
|
||||||
|
cmd, err := s.conn.Exec(ctx, `DELETE FROM mobile_app_versions WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.RowsAffected() == 0 {
|
||||||
|
return pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLatestActiveMobileAppVersion(ctx context.Context, platform string) (domain.MobileAppVersion, error) {
|
||||||
|
row := s.conn.QueryRow(ctx, `
|
||||||
|
SELECT `+mobileAppVersionSelectCols+`
|
||||||
|
FROM mobile_app_versions
|
||||||
|
WHERE platform = $1
|
||||||
|
AND status = 'ACTIVE'
|
||||||
|
ORDER BY version_code DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, platform)
|
||||||
|
return scanMobileAppVersion(row)
|
||||||
|
}
|
||||||
237
internal/services/appversions/service.go
Normal file
237
internal/services/appversions/service.go
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -247,6 +247,13 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{Key: "faqs.update", Name: "Update FAQ", Description: "Update a FAQ item", GroupName: "FAQs"},
|
{Key: "faqs.update", Name: "Update FAQ", Description: "Update a FAQ item", GroupName: "FAQs"},
|
||||||
{Key: "faqs.delete", Name: "Delete FAQ", Description: "Delete a FAQ item", GroupName: "FAQs"},
|
{Key: "faqs.delete", Name: "Delete FAQ", Description: "Delete a FAQ item", GroupName: "FAQs"},
|
||||||
|
|
||||||
|
// Mobile app versions
|
||||||
|
{Key: "app_versions.create", Name: "Create App Version", Description: "Create a mobile app version release", GroupName: "App Versions"},
|
||||||
|
{Key: "app_versions.list", Name: "List App Versions", Description: "List mobile app versions for admin management", GroupName: "App Versions"},
|
||||||
|
{Key: "app_versions.get", Name: "Get App Version", Description: "Get mobile app version by ID", GroupName: "App Versions"},
|
||||||
|
{Key: "app_versions.update", Name: "Update App Version", Description: "Update a mobile app version release", GroupName: "App Versions"},
|
||||||
|
{Key: "app_versions.delete", Name: "Delete App Version", Description: "Delete a mobile app version release", GroupName: "App Versions"},
|
||||||
|
|
||||||
// Email templates
|
// Email templates
|
||||||
{Key: "email_templates.create", Name: "Create Email Template", Description: "Create an email template", GroupName: "Email Templates"},
|
{Key: "email_templates.create", Name: "Create Email Template", Description: "Create an email template", GroupName: "Email Templates"},
|
||||||
{Key: "email_templates.list", Name: "List Email Templates", Description: "List email templates for admin management", GroupName: "Email Templates"},
|
{Key: "email_templates.list", Name: "List Email Templates", Description: "List email templates for admin management", GroupName: "Email Templates"},
|
||||||
|
|
@ -468,6 +475,9 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
// FAQs
|
// FAQs
|
||||||
"faqs.create", "faqs.list", "faqs.get", "faqs.update", "faqs.delete",
|
"faqs.create", "faqs.list", "faqs.get", "faqs.update", "faqs.delete",
|
||||||
|
|
||||||
|
// Mobile app versions
|
||||||
|
"app_versions.create", "app_versions.list", "app_versions.get", "app_versions.update", "app_versions.delete",
|
||||||
|
|
||||||
// Email templates
|
// Email templates
|
||||||
"email_templates.create", "email_templates.list", "email_templates.get", "email_templates.update", "email_templates.delete", "email_templates.preview",
|
"email_templates.create", "email_templates.list", "email_templates.get", "email_templates.update", "email_templates.delete", "email_templates.preview",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
|
"Yimaru-Backend/internal/services/appversions"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/chapa"
|
"Yimaru-Backend/internal/services/chapa"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
|
|
@ -53,6 +54,7 @@ type App struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
faqSvc *faqs.Service
|
||||||
|
appVersionSvc *appversions.Service
|
||||||
emailTemplateSvc *emailtemplates.Service
|
emailTemplateSvc *emailtemplates.Service
|
||||||
profileFieldOptionSvc *profilefieldoptions.Service
|
profileFieldOptionSvc *profilefieldoptions.Service
|
||||||
personaSvc *personas.Service
|
personaSvc *personas.Service
|
||||||
|
|
@ -97,6 +99,7 @@ func NewApp(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
faqSvc *faqs.Service,
|
||||||
|
appVersionSvc *appversions.Service,
|
||||||
emailTemplateSvc *emailtemplates.Service,
|
emailTemplateSvc *emailtemplates.Service,
|
||||||
profileFieldOptionSvc *profilefieldoptions.Service,
|
profileFieldOptionSvc *profilefieldoptions.Service,
|
||||||
personaSvc *personas.Service,
|
personaSvc *personas.Service,
|
||||||
|
|
@ -153,6 +156,7 @@ func NewApp(
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
faqSvc: faqSvc,
|
||||||
|
appVersionSvc: appVersionSvc,
|
||||||
emailTemplateSvc: emailTemplateSvc,
|
emailTemplateSvc: emailTemplateSvc,
|
||||||
profileFieldOptionSvc: profileFieldOptionSvc,
|
profileFieldOptionSvc: profileFieldOptionSvc,
|
||||||
personaSvc: personaSvc,
|
personaSvc: personaSvc,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
|
"Yimaru-Backend/internal/services/appversions"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/chapa"
|
"Yimaru-Backend/internal/services/chapa"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
|
|
@ -52,6 +53,7 @@ type Handler struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
faqSvc *faqs.Service
|
||||||
|
appVersionSvc *appversions.Service
|
||||||
emailTemplateSvc *emailtemplates.Service
|
emailTemplateSvc *emailtemplates.Service
|
||||||
profileFieldOptionSvc *profilefieldoptions.Service
|
profileFieldOptionSvc *profilefieldoptions.Service
|
||||||
personaSvc *personas.Service
|
personaSvc *personas.Service
|
||||||
|
|
@ -92,6 +94,7 @@ func New(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
faqSvc *faqs.Service,
|
||||||
|
appVersionSvc *appversions.Service,
|
||||||
emailTemplateSvc *emailtemplates.Service,
|
emailTemplateSvc *emailtemplates.Service,
|
||||||
profileFieldOptionSvc *profilefieldoptions.Service,
|
profileFieldOptionSvc *profilefieldoptions.Service,
|
||||||
personaSvc *personas.Service,
|
personaSvc *personas.Service,
|
||||||
|
|
@ -131,6 +134,7 @@ func New(
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
faqSvc: faqSvc,
|
||||||
|
appVersionSvc: appVersionSvc,
|
||||||
emailTemplateSvc: emailTemplateSvc,
|
emailTemplateSvc: emailTemplateSvc,
|
||||||
profileFieldOptionSvc: profileFieldOptionSvc,
|
profileFieldOptionSvc: profileFieldOptionSvc,
|
||||||
personaSvc: personaSvc,
|
personaSvc: personaSvc,
|
||||||
|
|
|
||||||
390
internal/web_server/handlers/mobile_app_version_handler.go
Normal file
390
internal/web_server/handlers/mobile_app_version_handler.go
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.assessmentSvc,
|
a.assessmentSvc,
|
||||||
a.questionsSvc,
|
a.questionsSvc,
|
||||||
a.faqSvc,
|
a.faqSvc,
|
||||||
|
a.appVersionSvc,
|
||||||
a.emailTemplateSvc,
|
a.emailTemplateSvc,
|
||||||
a.profileFieldOptionSvc,
|
a.profileFieldOptionSvc,
|
||||||
a.personaSvc,
|
a.personaSvc,
|
||||||
|
|
@ -200,6 +201,14 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Put("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.update"), h.UpdateFAQ)
|
groupV1.Put("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.update"), h.UpdateFAQ)
|
||||||
groupV1.Delete("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.delete"), h.DeleteFAQ)
|
groupV1.Delete("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.delete"), h.DeleteFAQ)
|
||||||
|
|
||||||
|
// Mobile app versions (Play Store / App Store)
|
||||||
|
groupV1.Get("/app/version/check", h.CheckMobileAppVersion)
|
||||||
|
groupV1.Get("/admin/app-versions", a.authMiddleware, a.RequirePermission("app_versions.list"), h.ListMobileAppVersionsAdmin)
|
||||||
|
groupV1.Get("/admin/app-versions/:id", a.authMiddleware, a.RequirePermission("app_versions.get"), h.GetMobileAppVersionByIDAdmin)
|
||||||
|
groupV1.Post("/admin/app-versions", a.authMiddleware, a.RequirePermission("app_versions.create"), h.CreateMobileAppVersion)
|
||||||
|
groupV1.Put("/admin/app-versions/:id", a.authMiddleware, a.RequirePermission("app_versions.update"), h.UpdateMobileAppVersion)
|
||||||
|
groupV1.Delete("/admin/app-versions/:id", a.authMiddleware, a.RequirePermission("app_versions.delete"), h.DeleteMobileAppVersion)
|
||||||
|
|
||||||
// Email templates
|
// Email templates
|
||||||
groupV1.Get("/admin/email-templates", a.authMiddleware, a.RequirePermission("email_templates.list"), h.ListEmailTemplatesAdmin)
|
groupV1.Get("/admin/email-templates", a.authMiddleware, a.RequirePermission("email_templates.list"), h.ListEmailTemplatesAdmin)
|
||||||
groupV1.Get("/admin/email-templates/slug/:slug", a.authMiddleware, a.RequirePermission("email_templates.get"), h.GetEmailTemplateBySlugAdmin)
|
groupV1.Get("/admin/email-templates/slug/:slug", a.authMiddleware, a.RequirePermission("email_templates.get"), h.GetEmailTemplateBySlugAdmin)
|
||||||
|
|
|
||||||
848
postman/Mobile-App-Versions.postman_collection.json
Normal file
848
postman/Mobile-App-Versions.postman_collection.json
Normal file
|
|
@ -0,0 +1,848 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "c4e8a1b2-7f3d-4a9e-b6c1-2d8f9e0a1b2c",
|
||||||
|
"name": "Mobile App Versions - Complete Flow",
|
||||||
|
"description": "Complete collection for mobile app version management: public version check (Play Store / App Store) and admin CRUD with force vs optional update policies.",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{access_token}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "http://localhost:8080"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "app_version_id",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "app_version_id_optional",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client_version_code",
|
||||||
|
"value": "10"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "01 - Admin App Version CRUD",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Android Version (FORCE update)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 201\", function () {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.test(\"App version ID exists\", function () {",
|
||||||
|
" pm.expect(body.data.id).to.be.a(\"number\");",
|
||||||
|
"});",
|
||||||
|
"pm.test(\"Update type is FORCE\", function () {",
|
||||||
|
" pm.expect(body.data.update_type).to.eql(\"FORCE\");",
|
||||||
|
"});",
|
||||||
|
"pm.collectionVariables.set(\"app_version_id\", body.data.id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"platform\": \"ANDROID\",\n \"version_name\": \"1.3.0\",\n \"version_code\": 15,\n \"update_type\": \"FORCE\",\n \"release_notes\": \"Critical security update and performance improvements.\",\n \"store_url\": \"https://play.google.com/store/apps/details?id=com.yimaru.app\",\n \"min_supported_version_code\": 12,\n \"status\": \"ACTIVE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Android Version (OPTIONAL update)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 201\", function () {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.collectionVariables.set(\"app_version_id_optional\", body.data.id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"platform\": \"ANDROID\",\n \"version_name\": \"1.2.0\",\n \"version_code\": 12,\n \"update_type\": \"OPTIONAL\",\n \"release_notes\": \"Minor bug fixes.\",\n \"store_url\": \"https://play.google.com/store/apps/details?id=com.yimaru.app\",\n \"status\": \"ACTIVE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create iOS Version (OPTIONAL update)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"platform\": \"IOS\",\n \"version_name\": \"1.3.0\",\n \"version_code\": 15,\n \"update_type\": \"OPTIONAL\",\n \"release_notes\": \"New lessons and UI polish.\",\n \"store_url\": \"https://apps.apple.com/app/id000000000\",\n \"status\": \"ACTIVE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List App Versions (Admin - All)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions?limit=20&offset=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "limit",
|
||||||
|
"value": "20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List App Versions (Admin - ANDROID only)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions?platform=ANDROID&limit=20&offset=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "ANDROID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "limit",
|
||||||
|
"value": "20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List App Versions (Admin - ACTIVE only)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions?status=ACTIVE&limit=20&offset=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "status",
|
||||||
|
"value": "ACTIVE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "limit",
|
||||||
|
"value": "20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get App Version By ID (Admin)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/{{app_version_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"{{app_version_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update App Version (Admin - change to OPTIONAL)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.test(\"Update type is OPTIONAL\", function () {",
|
||||||
|
" pm.expect(body.data.update_type).to.eql(\"OPTIONAL\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"update_type\": \"OPTIONAL\",\n \"release_notes\": \"Updated policy: optional update for this release.\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/{{app_version_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"{{app_version_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update App Version (Admin - set INACTIVE)",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"status\": \"INACTIVE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/{{app_version_id_optional}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"{{app_version_id_optional}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "02 - Public Mobile Version Check",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Check Version - Client up to date",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.test(\"No update available\", function () {",
|
||||||
|
" pm.expect(body.data.update_available).to.eql(false);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?platform=ANDROID&version_code=15",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "ANDROID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "15"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check Version - Update available (optional)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.test(\"Update available\", function () {",
|
||||||
|
" pm.expect(body.data.update_available).to.eql(true);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?platform=ANDROID&version_code={{client_version_code}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "ANDROID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "{{client_version_code}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check Version - Force update (below min_supported)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.test(\"Force update required\", function () {",
|
||||||
|
" pm.expect(body.data.force_update).to.eql(true);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?platform=ANDROID&version_code=5",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "ANDROID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check Version - iOS",
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?platform=IOS&version_code=10",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "IOS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "03 - Validation & Auth Errors",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Version Missing platform - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"version_name\": \"1.0.0\",\n \"version_code\": 1,\n \"update_type\": \"OPTIONAL\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Version Invalid update_type - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"platform\": \"ANDROID\",\n \"version_name\": \"1.0.0\",\n \"version_code\": 1,\n \"update_type\": \"MANDATORY\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check Version Missing platform - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?version_code=10",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check Version Invalid version_code - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/app/version/check?platform=ANDROID&version_code=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"app",
|
||||||
|
"version",
|
||||||
|
"check"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "platform",
|
||||||
|
"value": "ANDROID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "version_code",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List Admin Versions Without Auth - Expect 401/403",
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Missing App Version (Admin) - Expect 404",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 404\", function () {",
|
||||||
|
" pm.response.to.have.status(404);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/99999999",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"99999999"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "04 - Cleanup",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Delete App Version (FORCE/updated)",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/{{app_version_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"{{app_version_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete App Version (OPTIONAL/inactive)",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/admin/app-versions/{{app_version_id_optional}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"admin",
|
||||||
|
"app-versions",
|
||||||
|
"{{app_version_id_optional}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user