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"
|
||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||
"Yimaru-Backend/internal/services/arifpay"
|
||||
"Yimaru-Backend/internal/services/appversions"
|
||||
"Yimaru-Backend/internal/services/chapa"
|
||||
"Yimaru-Backend/internal/services/assessment"
|
||||
"Yimaru-Backend/internal/services/authentication"
|
||||
|
|
@ -400,6 +401,7 @@ func main() {
|
|||
// Questions service (unified questions system)
|
||||
questionsSvc := questions.NewService(store)
|
||||
faqSvc := faqs.NewService(repository.NewFAQStore(store))
|
||||
appVersionSvc := appversions.NewService(repository.NewMobileAppVersionStore(store))
|
||||
personasSvc := personasservice.NewService(store)
|
||||
examPrepSvc := examprep.NewService(store)
|
||||
|
||||
|
|
@ -480,6 +482,7 @@ func main() {
|
|||
assessmentSvc,
|
||||
questionsSvc,
|
||||
faqSvc,
|
||||
appVersionSvc,
|
||||
emailTemplateSvc,
|
||||
profileFieldOptionSvc,
|
||||
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', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
|
||||
('occupation', 'STUDENT', 'Student', 1, 'ACTIVE'),
|
||||
('occupation', 'EMPLOYED', 'Employed', 2, 'ACTIVE'),
|
||||
('occupation', 'SELF_EMPLOYED', 'Self-employed', 3, 'ACTIVE'),
|
||||
('occupation', 'UNEMPLOYED', 'Unemployed', 4, 'ACTIVE'),
|
||||
('occupation', 'HOMEMAKER', 'Homemaker', 5, 'ACTIVE'),
|
||||
('occupation', 'RETIRED', 'Retired', 6, 'ACTIVE'),
|
||||
('occupation', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
('occupation', 'STUDENTS', 'Students (High school & University)', 1, 'ACTIVE'),
|
||||
('occupation', 'JOB_SEEKERS', 'Job Seekers / Fresh Graduates', 2, 'ACTIVE'),
|
||||
('occupation', 'WORKING_PROFESSIONALS', 'Working Professionals (Corporate/Office)', 3, 'ACTIVE'),
|
||||
('occupation', 'GOVERNMENT_NGO', 'Government & NGO Workers', 4, 'ACTIVE'),
|
||||
('occupation', 'ENTREPRENEURS', 'Entrepreneurs & Small Business Owners', 5, 'ACTIVE'),
|
||||
('occupation', 'HOSPITALITY_TOURISM', 'Hospitality & Tourism Workers', 6, 'ACTIVE'),
|
||||
('occupation', 'FREELANCERS_REMOTE', 'Freelancers / Remote Workers (Digital Economy)', 7, 'ACTIVE'),
|
||||
|
||||
('age_group', 'UNDER_13', 'Under 13', 1, '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'),
|
||||
|
||||
('language_challange', 'PRONUNCIATION', 'Pronunciation', 1, 'ACTIVE'),
|
||||
('language_challange', 'GRAMMAR', 'Grammar', 2, 'ACTIVE'),
|
||||
('language_challange', 'VOCABULARY', 'Vocabulary', 3, 'ACTIVE'),
|
||||
('language_challange', 'LISTENING', 'Listening', 4, 'ACTIVE'),
|
||||
('language_challange', 'SPEAKING', 'Speaking confidence', 5, 'ACTIVE'),
|
||||
('language_challange', 'WRITING', 'Writing', 6, 'ACTIVE'),
|
||||
('language_challange', 'READING', 'Reading', 7, 'ACTIVE'),
|
||||
('language_challange', 'WORDS_GRAMMAR', 'Finding words or grammar quickly', 2, 'ACTIVE'),
|
||||
('language_challange', 'CONFIDENCE', 'Feeling nervous or lacking confidence', 3, 'ACTIVE'),
|
||||
('language_challange', 'ACCENTS_FAST_SPEECH', 'Understanding accents or fast speech', 4, 'ACTIVE'),
|
||||
('language_challange', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
|
||||
('language_goal', 'BASIC', 'Basic communication', 1, 'ACTIVE'),
|
||||
('language_goal', 'CONVERSATIONAL', 'Conversational fluency', 2, 'ACTIVE'),
|
||||
('language_goal', 'PROFESSIONAL', 'Professional proficiency', 3, 'ACTIVE'),
|
||||
('language_goal', 'ACADEMIC', 'Academic proficiency', 4, 'ACTIVE'),
|
||||
('language_goal', 'NATIVE_LIKE', 'Near-native fluency', 5, 'ACTIVE'),
|
||||
('language_goal', 'SPEAK_CONFIDENTLY', 'Speak confidently at work or school', 1, 'ACTIVE'),
|
||||
('language_goal', 'TRAVEL_DAILY', 'Travel or handle daily situations', 2, 'ACTIVE'),
|
||||
('language_goal', 'FAMILY_FRIENDS', 'Connect with family or friends', 3, 'ACTIVE'),
|
||||
('language_goal', 'GENERAL_SKILLS', 'General skills expansion', 4, 'ACTIVE'),
|
||||
('language_goal', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
|
||||
('favourite_topic', 'BUSINESS', 'Business', 1, 'ACTIVE'),
|
||||
('favourite_topic', 'TECHNOLOGY', 'Technology', 2, 'ACTIVE'),
|
||||
('favourite_topic', 'HEALTH', 'Health', 3, 'ACTIVE'),
|
||||
('favourite_topic', 'CULTURE', 'Culture', 4, 'ACTIVE'),
|
||||
('favourite_topic', 'TRAVEL', 'Travel', 5, 'ACTIVE'),
|
||||
('favourite_topic', 'ENTERTAINMENT', 'Entertainment', 6, 'ACTIVE'),
|
||||
('favourite_topic', 'FOOD_COOKING', 'Food & Cooking', 1, 'ACTIVE'),
|
||||
('favourite_topic', 'HOBBIES_SPORTS_MUSIC', 'Hobbies, Sports, Music', 2, 'ACTIVE'),
|
||||
('favourite_topic', 'TECH_NEWS_BUSINESS', 'Tech, News, Business', 3, 'ACTIVE'),
|
||||
('favourite_topic', 'TRAVEL_PLACES_CULTURE', 'Travel, Places, Culture', 4, 'ACTIVE'),
|
||||
('favourite_topic', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
|
||||
('country', 'ET', 'Ethiopia', 1, 'ACTIVE'),
|
||||
('country', 'ER', 'Eritrea', 2, 'ACTIVE'),
|
||||
('country', 'DJ', 'Djibouti', 3, 'ACTIVE'),
|
||||
('country', 'SO', 'Somalia', 4, 'ACTIVE'),
|
||||
('country', 'KE', 'Kenya', 5, 'ACTIVE'),
|
||||
('country', 'SD', 'Sudan', 6, 'ACTIVE'),
|
||||
('country', 'SS', 'South Sudan', 7, 'ACTIVE'),
|
||||
('country', 'UG', 'Uganda', 8, 'ACTIVE'),
|
||||
('country', 'RW', 'Rwanda', 9, 'ACTIVE'),
|
||||
('country', 'TZ', 'Tanzania', 10, 'ACTIVE'),
|
||||
('country', 'EG', 'Egypt', 11, 'ACTIVE'),
|
||||
('country', 'NG', 'Nigeria', 12, 'ACTIVE'),
|
||||
('country', 'ZA', 'South Africa', 13, 'ACTIVE'),
|
||||
('country', 'US', 'United States', 20, 'ACTIVE'),
|
||||
('country', 'GB', 'United Kingdom', 21, 'ACTIVE'),
|
||||
('country', 'CA', 'Canada', 22, 'ACTIVE'),
|
||||
('country', 'DE', 'Germany', 23, 'ACTIVE'),
|
||||
('country', 'FR', 'France', 24, 'ACTIVE'),
|
||||
('country', 'IN', 'India', 25, 'ACTIVE'),
|
||||
('country', 'CN', 'China', 26, 'ACTIVE'),
|
||||
('country', 'SA', 'Saudi Arabia', 27, 'ACTIVE'),
|
||||
('country', 'AE', 'United Arab Emirates', 28, 'ACTIVE'),
|
||||
('country', 'OTHER', 'Other', 99, 'ACTIVE'),
|
||||
('country', 'AF', 'Afghanistan', 1, 'ACTIVE'),
|
||||
('country', 'AL', 'Albania', 2, 'ACTIVE'),
|
||||
('country', 'DZ', 'Algeria', 3, 'ACTIVE'),
|
||||
('country', 'AD', 'Andorra', 4, 'ACTIVE'),
|
||||
('country', 'AO', 'Angola', 5, 'ACTIVE'),
|
||||
('country', 'AR', 'Argentina', 6, 'ACTIVE'),
|
||||
('country', 'AM', 'Armenia', 7, 'ACTIVE'),
|
||||
('country', 'AU', 'Australia', 8, 'ACTIVE'),
|
||||
('country', 'AT', 'Austria', 9, 'ACTIVE'),
|
||||
('country', 'AZ', 'Azerbaijan', 10, 'ACTIVE'),
|
||||
('country', 'BH', 'Bahrain', 11, 'ACTIVE'),
|
||||
('country', 'BD', 'Bangladesh', 12, 'ACTIVE'),
|
||||
('country', 'BY', 'Belarus', 13, 'ACTIVE'),
|
||||
('country', 'BE', 'Belgium', 14, 'ACTIVE'),
|
||||
('country', 'BZ', 'Belize', 15, 'ACTIVE'),
|
||||
('country', 'BJ', 'Benin', 16, 'ACTIVE'),
|
||||
('country', 'BT', 'Bhutan', 17, 'ACTIVE'),
|
||||
('country', 'BO', 'Bolivia', 18, 'ACTIVE'),
|
||||
('country', 'BA', 'Bosnia and Herzegovina', 19, 'ACTIVE'),
|
||||
('country', 'BW', 'Botswana', 20, 'ACTIVE'),
|
||||
('country', 'BR', 'Brazil', 21, 'ACTIVE'),
|
||||
('country', 'BN', 'Brunei', 22, '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', 'DIRE_DAWA', 'Dire Dawa', 2, 'ACTIVE'),
|
||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 3, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AFAR', 'Afar', 4, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AMHARA', 'Amhara', 5, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OROMIA', 'Oromia', 6, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOMALI', 'Somali', 7, 'ACTIVE'),
|
||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 8, 'ACTIVE'),
|
||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 9, 'ACTIVE'),
|
||||
('ethiopia_regions', 'HARARI', 'Harari', 10, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 11, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AFAR', 'Afar', 2, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AMHARA', 'Amhara', 3, 'ACTIVE'),
|
||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 4, 'ACTIVE'),
|
||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 5, 'ACTIVE'),
|
||||
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 6, 'ACTIVE'),
|
||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 7, 'ACTIVE'),
|
||||
('ethiopia_regions', 'HARARI', 'Harari', 8, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OROMIA', 'Oromia', 9, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 10, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOMALI', 'Somali', 11, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA', 'South West Ethiopia', 13, 'ACTIVE'),
|
||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 14, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OTHER', 'Other', 99, 'ACTIVE');
|
||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA_PEOPLES', 'South West Ethiopia Peoples', 13, 'ACTIVE'),
|
||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 14, 'ACTIVE');
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
INSERT INTO field_options (field_key, code, label, display_order, status) VALUES
|
||||
('ethiopia_regions', 'ADDIS_ABABA', 'Addis Ababa', 1, 'ACTIVE'),
|
||||
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 2, 'ACTIVE'),
|
||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 3, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AFAR', 'Afar', 4, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AMHARA', 'Amhara', 5, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OROMIA', 'Oromia', 6, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOMALI', 'Somali', 7, 'ACTIVE'),
|
||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 8, 'ACTIVE'),
|
||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 9, 'ACTIVE'),
|
||||
('ethiopia_regions', 'HARARI', 'Harari', 10, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 11, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AFAR', 'Afar', 2, 'ACTIVE'),
|
||||
('ethiopia_regions', 'AMHARA', 'Amhara', 3, 'ACTIVE'),
|
||||
('ethiopia_regions', 'BENISHANGUL_GUMUZ', 'Benishangul-Gumuz', 4, 'ACTIVE'),
|
||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 5, 'ACTIVE'),
|
||||
('ethiopia_regions', 'DIRE_DAWA', 'Dire Dawa', 6, 'ACTIVE'),
|
||||
('ethiopia_regions', 'GAMBELA', 'Gambela', 7, 'ACTIVE'),
|
||||
('ethiopia_regions', 'HARARI', 'Harari', 8, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OROMIA', 'Oromia', 9, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SIDAMA', 'Sidama', 10, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOMALI', 'Somali', 11, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOUTH_ETHIOPIA', 'South Ethiopia', 12, 'ACTIVE'),
|
||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA', 'South West Ethiopia', 13, 'ACTIVE'),
|
||||
('ethiopia_regions', 'CENTRAL_ETHIOPIA', 'Central Ethiopia', 14, 'ACTIVE'),
|
||||
('ethiopia_regions', 'OTHER', 'Other', 99, 'ACTIVE')
|
||||
ON CONFLICT (field_key, code) DO NOTHING;
|
||||
('ethiopia_regions', 'SOUTH_WEST_ETHIOPIA_PEOPLES', 'South West Ethiopia Peoples', 13, 'ACTIVE'),
|
||||
('ethiopia_regions', 'TIGRAY', 'Tigray', 14, 'ACTIVE')
|
||||
ON CONFLICT (field_key, code) DO UPDATE SET
|
||||
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.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
|
||||
{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"},
|
||||
|
|
@ -468,6 +475,9 @@ var DefaultRolePermissions = map[string][]string{
|
|||
// FAQs
|
||||
"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.create", "email_templates.list", "email_templates.get", "email_templates.update", "email_templates.delete", "email_templates.preview",
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package httpserver
|
|||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"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/chapa"
|
||||
"Yimaru-Backend/internal/services/assessment"
|
||||
|
|
@ -53,6 +54,7 @@ type App struct {
|
|||
assessmentSvc *assessment.Service
|
||||
questionsSvc *questions.Service
|
||||
faqSvc *faqs.Service
|
||||
appVersionSvc *appversions.Service
|
||||
emailTemplateSvc *emailtemplates.Service
|
||||
profileFieldOptionSvc *profilefieldoptions.Service
|
||||
personaSvc *personas.Service
|
||||
|
|
@ -97,6 +99,7 @@ func NewApp(
|
|||
assessmentSvc *assessment.Service,
|
||||
questionsSvc *questions.Service,
|
||||
faqSvc *faqs.Service,
|
||||
appVersionSvc *appversions.Service,
|
||||
emailTemplateSvc *emailtemplates.Service,
|
||||
profileFieldOptionSvc *profilefieldoptions.Service,
|
||||
personaSvc *personas.Service,
|
||||
|
|
@ -153,6 +156,7 @@ func NewApp(
|
|||
assessmentSvc: assessmentSvc,
|
||||
questionsSvc: questionsSvc,
|
||||
faqSvc: faqSvc,
|
||||
appVersionSvc: appVersionSvc,
|
||||
emailTemplateSvc: emailTemplateSvc,
|
||||
profileFieldOptionSvc: profileFieldOptionSvc,
|
||||
personaSvc: personaSvc,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import (
|
|||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/config"
|
||||
"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/chapa"
|
||||
"Yimaru-Backend/internal/services/assessment"
|
||||
|
|
@ -52,6 +53,7 @@ type Handler struct {
|
|||
assessmentSvc *assessment.Service
|
||||
questionsSvc *questions.Service
|
||||
faqSvc *faqs.Service
|
||||
appVersionSvc *appversions.Service
|
||||
emailTemplateSvc *emailtemplates.Service
|
||||
profileFieldOptionSvc *profilefieldoptions.Service
|
||||
personaSvc *personas.Service
|
||||
|
|
@ -92,6 +94,7 @@ func New(
|
|||
assessmentSvc *assessment.Service,
|
||||
questionsSvc *questions.Service,
|
||||
faqSvc *faqs.Service,
|
||||
appVersionSvc *appversions.Service,
|
||||
emailTemplateSvc *emailtemplates.Service,
|
||||
profileFieldOptionSvc *profilefieldoptions.Service,
|
||||
personaSvc *personas.Service,
|
||||
|
|
@ -131,6 +134,7 @@ func New(
|
|||
assessmentSvc: assessmentSvc,
|
||||
questionsSvc: questionsSvc,
|
||||
faqSvc: faqSvc,
|
||||
appVersionSvc: appVersionSvc,
|
||||
emailTemplateSvc: emailTemplateSvc,
|
||||
profileFieldOptionSvc: profileFieldOptionSvc,
|
||||
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.questionsSvc,
|
||||
a.faqSvc,
|
||||
a.appVersionSvc,
|
||||
a.emailTemplateSvc,
|
||||
a.profileFieldOptionSvc,
|
||||
a.personaSvc,
|
||||
|
|
@ -200,6 +201,14 @@ func (a *App) initAppRoutes() {
|
|||
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)
|
||||
|
||||
// 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
|
||||
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)
|
||||
|
|
|
|||
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