lifedebugger commited on
Commit
decc167
·
1 Parent(s): 5a84566

Deploy files from GitHub repository

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. main.go +0 -4
  2. pkg/storage/local.go +43 -41
  3. response/api_response_v2.go +2 -1
  4. services/forgot_password_service.go +1 -1
  5. space/config/config.go +214 -44
  6. space/main.go +24 -23
  7. space/models/exception_model.go +1 -0
  8. space/repositories/academy_repository.go +16 -17
  9. space/repositories/quiz_repository.go +91 -71
  10. space/response/api_response_v2.go +13 -1
  11. space/services/email_service.go +1 -1
  12. space/services/forgot_password_service.go +5 -4
  13. space/services/jwt_service.go +5 -8
  14. space/services/quiz_service.go +196 -134
  15. space/space/controller/quiz/quiz_controller.go +24 -0
  16. space/space/models/request_model.go +12 -1
  17. space/space/router/quiz_route.go +1 -0
  18. space/space/services/quiz_service.go +203 -124
  19. space/space/space/repositories/quiz_repository.go +2 -1
  20. space/space/space/space/models/field_counter.go +1 -1
  21. space/space/space/space/pkg/validation/validation.go +2 -2
  22. space/space/space/space/services/cv_service.go +7 -2
  23. space/space/space/space/space/controller/quiz/quiz_controller.go +142 -4
  24. space/space/space/space/space/models/database_orm_model.go +19 -13
  25. space/space/space/space/space/models/request_model.go +85 -20
  26. space/space/space/space/space/repositories/quiz_repository.go +275 -33
  27. space/space/space/space/space/router/quiz_route.go +6 -3
  28. space/space/space/space/space/services/quiz_service.go +187 -68
  29. space/space/space/space/space/space/response/api_response_v2.go +9 -0
  30. space/space/space/space/space/space/space/config/database_connection_config.go +3 -1
  31. space/space/space/space/space/space/space/controller/quiz/quiz_controller.go +76 -0
  32. space/space/space/space/space/space/space/main.go +6 -0
  33. space/space/space/space/space/space/space/models/database_orm_model.go +127 -66
  34. space/space/space/space/space/space/space/models/exception_model.go +6 -0
  35. space/space/space/space/space/space/space/models/field_counter.go +40 -0
  36. space/space/space/space/space/space/space/models/request_model.go +30 -18
  37. space/space/space/space/space/space/space/models/response_model.go +0 -5
  38. space/space/space/space/space/space/space/repositories/academy_repository.go +48 -0
  39. space/space/space/space/space/space/space/repositories/quiz_repository.go +111 -118
  40. space/space/space/space/space/space/space/router/quiz_route.go +5 -13
  41. space/space/space/space/space/space/space/router/router.go +1 -1
  42. space/space/space/space/space/space/space/router/server.go +4 -0
  43. space/space/space/space/space/space/space/services/cv_service.go +22 -14
  44. space/space/space/space/space/space/space/services/quiz_service.go +198 -35
  45. space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go +11 -5
  46. space/space/space/space/space/space/space/space/space/models/database_orm_model.go +3 -1
  47. space/space/space/space/space/space/space/space/space/models/request_model.go +56 -4
  48. space/space/space/space/space/space/space/space/space/repositories/academy_repository.go +21 -13
  49. space/space/space/space/space/space/space/space/space/repositories/quiz_repository.go +19 -0
  50. space/space/space/space/space/space/space/space/space/router/quiz_route.go +1 -1
main.go CHANGED
@@ -1,7 +1,6 @@
1
  package main
2
 
3
  import (
4
- "fmt"
5
  "log/slog"
6
  "net"
7
  "strconv"
@@ -32,9 +31,6 @@ func main() {
32
  err := config.LoadConfig()
33
  utils.FatalIfErr("failed to load config", err)
34
 
35
- // print the all config loaded
36
- fmt.Printf("config: %+v", config.GlobalConfig)
37
-
38
  // setup validator for validation request
39
  validator, err := validation.New(config.DB)
40
  utils.FatalIfErr("failed to setup validator", err)
 
1
  package main
2
 
3
  import (
 
4
  "log/slog"
5
  "net"
6
  "strconv"
 
31
  err := config.LoadConfig()
32
  utils.FatalIfErr("failed to load config", err)
33
 
 
 
 
34
  // setup validator for validation request
35
  validator, err := validation.New(config.DB)
36
  utils.FatalIfErr("failed to setup validator", err)
pkg/storage/local.go CHANGED
@@ -1,67 +1,69 @@
1
  package storage
2
 
3
  import (
4
- "context"
5
- "fmt"
6
- "io"
7
- "os"
8
- "path/filepath"
9
- "strings"
10
  )
11
 
12
  type LocalStorage struct {
13
- *BaseStorage
14
- basePath string
15
- baseURL string
16
  }
17
 
18
  func NewLocalStorage(basePath, baseURL string) *LocalStorage {
19
- return &LocalStorage{
20
- BaseStorage: NewBaseStorage(),
21
- basePath: basePath,
22
- baseURL: baseURL,
23
- }
24
  }
25
 
26
  func (s *LocalStorage) Upload(ctx context.Context, file io.ReadSeeker, path string) error {
27
- fullPath := filepath.Join(s.basePath, path)
28
 
29
- dir := filepath.Dir(fullPath)
30
- if err := os.MkdirAll(dir, 0755); err != nil {
31
- return err
32
- }
33
 
34
- dst, err := os.Create(fullPath)
35
- if err != nil {
36
- return err
37
- }
38
- defer dst.Close()
39
 
40
- if _, err := io.Copy(dst, file); err != nil {
41
- return err
42
- }
43
- return nil
44
  }
45
 
46
  func (s *LocalStorage) GetURL(path string) string {
47
- return filepath.Join(s.baseURL, path)
 
 
 
 
48
  }
49
 
50
  func (s *LocalStorage) Delete(ctx context.Context, path string) error {
51
- // Contoh input: "http://localhost:8080/storage/users/5/profile/filename.png"
52
 
53
- // Ambil bagian setelah "/storage/"
54
- const prefix = "/storage/"
55
- idx := strings.Index(path, prefix)
56
- if idx == -1 {
57
- return os.ErrNotExist
58
- }
59
 
60
- relativePath := path[idx+len(prefix):]
61
 
62
- // Gabungkan dengan basePath
63
- fullPath := filepath.Join(s.basePath, relativePath)
64
- fmt.Println(fullPath)
65
 
66
- return os.Remove(fullPath)
67
  }
 
1
  package storage
2
 
3
  import (
4
+ "context"
5
+ "io"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
 
9
  )
10
 
11
  type LocalStorage struct {
12
+ *BaseStorage
13
+ basePath string
14
+ baseURL string
15
  }
16
 
17
  func NewLocalStorage(basePath, baseURL string) *LocalStorage {
18
+ return &LocalStorage{
19
+ BaseStorage: NewBaseStorage(),
20
+ basePath: basePath,
21
+ baseURL: baseURL,
22
+ }
23
  }
24
 
25
  func (s *LocalStorage) Upload(ctx context.Context, file io.ReadSeeker, path string) error {
26
+ fullPath := filepath.Join(s.basePath, path)
27
 
28
+ dir := filepath.Dir(fullPath)
29
+ if err := os.MkdirAll(dir, 0755); err != nil {
30
+ return err
31
+ }
32
 
33
+ dst, err := os.Create(fullPath)
34
+ if err != nil {
35
+ return err
36
+ }
37
+ defer dst.Close()
38
 
39
+ if _, err := io.Copy(dst, file); err != nil {
40
+ return err
41
+ }
42
+ return nil
43
  }
44
 
45
  func (s *LocalStorage) GetURL(path string) string {
46
+ urlBuilder := strings.Builder{}
47
+ urlBuilder.Write([]byte(s.baseURL))
48
+ urlBuilder.Write([]byte(path))
49
+
50
+ return urlBuilder.String()
51
  }
52
 
53
  func (s *LocalStorage) Delete(ctx context.Context, path string) error {
54
+ // Contoh input: "http://localhost:8080/storage/users/5/profile/filename.png"
55
 
56
+ // Ambil bagian setelah "/storage/"
57
+ const prefix = "/storage/"
58
+ idx := strings.Index(path, prefix)
59
+ if idx == -1 {
60
+ return os.ErrNotExist
61
+ }
62
 
63
+ relativePath := path[idx+len(prefix):]
64
 
65
+ // Gabungkan dengan basePath
66
+ fullPath := filepath.Join(s.basePath, relativePath)
 
67
 
68
+ return os.Remove(fullPath)
69
  }
response/api_response_v2.go CHANGED
@@ -50,7 +50,8 @@ func HandleError(c *gin.Context, err error) {
50
  responseError(c, http.StatusBadRequest, exception)
51
  case exception.AcademyNotFinished:
52
  responseError(c, http.StatusBadRequest, exception)
53
-
 
54
  case exception.ValidationError:
55
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
56
  default:
 
50
  responseError(c, http.StatusBadRequest, exception)
51
  case exception.AcademyNotFinished:
52
  responseError(c, http.StatusBadRequest, exception)
53
+ case exception.QuizAlreadyPassed:
54
+ responseError(c, http.StatusBadRequest, exception)
55
  case exception.ValidationError:
56
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
57
  default:
services/forgot_password_service.go CHANGED
@@ -41,7 +41,7 @@ func (s *ForgotPasswordService) Create(email string) {
41
  return
42
  }
43
 
44
- remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Hour
45
  dueTime := CalculateDueTime(remainingTime)
46
 
47
  s.Constructor.UUID = uuid.NewV4()
 
41
  return
42
  }
43
 
44
+ remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
45
  dueTime := CalculateDueTime(remainingTime)
46
 
47
  s.Constructor.UUID = uuid.NewV4()
space/config/config.go CHANGED
@@ -1,66 +1,236 @@
1
  package config
2
 
3
  import (
 
 
4
  "os"
5
  "strconv"
6
  "time"
7
 
 
8
  "github.com/joho/godotenv"
 
 
9
  )
10
 
11
- var ENV string
12
- var TCP_ADDRESS string
13
- var LOG_PATH string
14
-
15
- var HOST_ADDRESS string
16
- var HOST_PORT string
17
- var EMAIL_VERIFICATION_DURATION int
18
-
19
- var SMTP_SENDER_EMAIL string
20
- var SMTP_SENDER_PASSWORD string
21
- var SMTP_HOST string
22
- var SMTP_PORT string
23
-
24
- var REDIS_HOST string
25
- var REDIS_PORT int
26
- var REDIS_PASSWORD string
27
- var REDIS_DB int
28
- var REDIS_MIN_IDLE_CONNS int
29
- var REDIS_POOL_SIZE int
30
- var REDIS_POOL_TIMEOUT time.Duration
31
-
32
- var APP_URL string
33
-
34
- func init() {
35
- godotenv.Load()
36
- ENV = os.Getenv("ENV")
37
- HOST_ADDRESS = os.Getenv("HOST_ADDRESS")
38
- HOST_PORT = os.Getenv("HOST_PORT")
39
- TCP_ADDRESS = HOST_ADDRESS + ":" + HOST_PORT
40
- LOG_PATH = os.Getenv("LOG_PATH")
41
- EMAIL_VERIFICATION_DURATION, _ = strconv.Atoi(os.Getenv("EMAIL_VERIFICATION_DURATION"))
42
- SMTP_SENDER_EMAIL = os.Getenv("SMTP_SENDER_EMAIL")
43
- SMTP_SENDER_PASSWORD = os.Getenv("SMTP_SENDER_PASSWORD")
44
- SMTP_HOST = os.Getenv("SMTP_HOST")
45
- SMTP_PORT = os.Getenv("SMTP_PORT")
46
-
47
- REDIS_HOST = getValue(os.Getenv("REDIS_HOST"), "redis", func(s string) (string, error) { return s, nil })
48
- REDIS_PORT = getValue(os.Getenv("REDIS_PORT"), 6379, func(s string) (int, error) { return strconv.Atoi(s) })
49
- REDIS_PASSWORD = getValue(os.Getenv("REDIS_PASSWORD"), "qobiltu", func(s string) (string, error) { return s, nil })
50
- REDIS_DB = getValue(os.Getenv("REDIS_DB"), 0, func(s string) (int, error) { return strconv.Atoi(s) })
51
- REDIS_POOL_SIZE = getValue(os.Getenv("REDIS_POOL_SIZE"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
52
- REDIS_MIN_IDLE_CONNS = getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
53
- REDIS_POOL_TIMEOUT = getValue(os.Getenv("REDIS_POOL_TIMEOUT"), time.Second*30, func(s string) (time.Duration, error) { return time.ParseDuration(s) })
54
- APP_URL = getValue(os.Getenv("APP_URL"), "http://localhost:3000", func(s string) (string, error) { return s, nil })
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
58
  if value == "" {
59
  return defaultValue
60
  }
61
  convertedValue, err := convert(value)
62
  if err != nil {
 
63
  return defaultValue
64
  }
65
  return convertedValue
66
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package config
2
 
3
  import (
4
+ "fmt"
5
+ "log"
6
  "os"
7
  "strconv"
8
  "time"
9
 
10
+ "api.qobiltu.id/models"
11
  "github.com/joho/godotenv"
12
+ "gorm.io/driver/postgres"
13
+ "gorm.io/gorm"
14
  )
15
 
16
+ // Config holds all configuration settings for the application.
17
+ type Config struct {
18
+ Database DatabaseConfig
19
+ SMTP SMTPConfig
20
+ Redis RedisConfig
21
+ App AppConfig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
+ // DatabaseConfig holds database connection settings.
25
+ type DatabaseConfig struct {
26
+ Host string
27
+ Port string
28
+ User string
29
+ Password string
30
+ Name string
31
+ DB *gorm.DB
32
+ }
33
+
34
+ // SMTPConfig holds SMTP settings for email services.
35
+ type SMTPConfig struct {
36
+ SenderEmail string
37
+ Password string
38
+ Host string
39
+ Port string
40
+ }
41
+
42
+ // RedisConfig holds Redis connection settings.
43
+ type RedisConfig struct {
44
+ Host string
45
+ Port int
46
+ Password string
47
+ DB int
48
+ MinIdleConns int
49
+ PoolSize int
50
+ PoolTimeout time.Duration
51
+ }
52
+
53
+ // AppConfig holds general application settings.
54
+ type AppConfig struct {
55
+ Env string
56
+ HostAddress string
57
+ HostPort string
58
+ TCPAddress string
59
+ LogPath string
60
+ EmailVerificationDuration int
61
+ AppURL string
62
+ Salt string
63
+ }
64
+
65
+ // GlobalConfig is the global configuration instance.
66
+ var GlobalConfig *Config
67
+ var DB *gorm.DB // Global DB instance for compatibility
68
+
69
+ // LoadConfig initializes the application configuration.
70
+ func LoadConfig() error {
71
+ // Load environment variables from .env file
72
+ if err := godotenv.Load(); err != nil {
73
+ log.Printf("Failed to load .env file: %v", err)
74
+ // Continue with defaults if .env file is not found
75
+ }
76
+
77
+ // Set timezone
78
+ if err := os.Setenv("TZ", "Asia/Jakarta"); err != nil {
79
+ return fmt.Errorf("failed to set timezone: %w", err)
80
+ }
81
+
82
+ cfg := &Config{
83
+ Database: DatabaseConfig{
84
+ Host: getValue(os.Getenv("DB_HOST"), "localhost", stringConverter),
85
+ Port: getValue(os.Getenv("DB_PORT"), "5432", stringConverter),
86
+ User: getValue(os.Getenv("DB_USER"), "postgres", stringConverter),
87
+ Password: getValue(os.Getenv("DB_PASSWORD"), "", stringConverter),
88
+ Name: getValue(os.Getenv("DB_NAME"), "qobiltu", stringConverter),
89
+ },
90
+ SMTP: SMTPConfig{
91
+ SenderEmail: getValue(os.Getenv("SMTP_SENDER_EMAIL"), "", stringConverter),
92
+ Password: getValue(os.Getenv("SMTP_SENDER_PASSWORD"), "", stringConverter),
93
+ Host: getValue(os.Getenv("SMTP_HOST"), "", stringConverter),
94
+ Port: getValue(os.Getenv("SMTP_PORT"), "", stringConverter),
95
+ },
96
+ Redis: RedisConfig{
97
+ Host: getValue(os.Getenv("REDIS_HOST"), "redis", stringConverter),
98
+ Port: getValue(os.Getenv("REDIS_PORT"), 6379, intConverter),
99
+ Password: getValue(os.Getenv("REDIS_PASSWORD"), "qobiltu", stringConverter),
100
+ DB: getValue(os.Getenv("REDIS_DB"), 0, intConverter),
101
+ MinIdleConns: getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, intConverter),
102
+ PoolSize: getValue(os.Getenv("REDIS_POOL_SIZE"), 10, intConverter),
103
+ PoolTimeout: getValue(os.Getenv("REDIS_POOL_TIMEOUT"), 30*time.Second, durationConverter),
104
+ },
105
+ App: AppConfig{
106
+ Env: getValue(os.Getenv("ENV"), "development", stringConverter),
107
+ HostAddress: getValue(os.Getenv("HOST_ADDRESS"), "localhost", stringConverter),
108
+ HostPort: getValue(os.Getenv("HOST_PORT"), "8080", stringConverter),
109
+ LogPath: getValue(os.Getenv("LOG_PATH"), "./logs", stringConverter),
110
+ EmailVerificationDuration: getValue(os.Getenv("EMAIL_VERIFICATION_DURATION"), 24, intConverter),
111
+ AppURL: getValue(os.Getenv("APP_URL"), "http://localhost:3000", stringConverter),
112
+ Salt: getValue(os.Getenv("SALT"), "D3f4u|t", stringConverter),
113
+ },
114
+ }
115
+
116
+ // Set TCP address
117
+ cfg.App.TCPAddress = fmt.Sprintf("%s:%s", cfg.App.HostAddress, cfg.App.HostPort)
118
+
119
+ // Initialize database connection
120
+ dsn := fmt.Sprintf(
121
+ "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta",
122
+ cfg.Database.Host, cfg.Database.User, cfg.Database.Password, cfg.Database.Name, cfg.Database.Port,
123
+ )
124
+
125
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
126
+ TranslateError: true,
127
+ // Logger: logger.Default.LogMode(logger.Info),
128
+ })
129
+ if err != nil {
130
+ return fmt.Errorf("failed to connect to database: %w", err)
131
+ }
132
+
133
+ // Configure database connection pooling
134
+ sqlDB, err := db.DB()
135
+ if err != nil {
136
+ return fmt.Errorf("failed to get sql.DB: %w", err)
137
+ }
138
+ sqlDB.SetMaxIdleConns(10)
139
+ sqlDB.SetMaxOpenConns(100)
140
+ sqlDB.SetConnMaxLifetime(time.Hour)
141
+ sqlDB.SetConnMaxIdleTime(30 * time.Minute)
142
+
143
+ // Assign database connection
144
+ cfg.Database.DB = db
145
+ DB = db
146
+
147
+ // Perform auto-migration
148
+ if err := AutoMigrateAll(db); err != nil {
149
+ return fmt.Errorf("auto-migration failed: %w", err)
150
+ }
151
+
152
+ // Assign to global config
153
+ GlobalConfig = cfg
154
+
155
+ return nil
156
+ }
157
+
158
+ // AutoMigrateAll performs auto-migration for all models and creates necessary sequences.
159
+ func AutoMigrateAll(db *gorm.DB) error {
160
+ // Auto-migrate all models
161
+ err := db.AutoMigrate(
162
+ &models.Account{},
163
+ &models.AccountDetails{},
164
+ &models.EmailVerification{},
165
+ &models.ExternalAuth{},
166
+ &models.FCM{},
167
+ &models.ForgotPassword{},
168
+ &models.Academy{},
169
+ &models.AcademyMaterial{},
170
+ &models.AcademyMaterialProgress{},
171
+ &models.RegionCity{},
172
+ &models.RegionProvince{},
173
+ &models.OptionCategory{},
174
+ &models.OptionValues{},
175
+ &models.Quiz{},
176
+ &models.Question{},
177
+ &models.Answer{},
178
+ &models.QuizAttempt{},
179
+ &models.UserAnswer{},
180
+ &models.PersonalityAndPreferenceCV{},
181
+ &models.FamilyMemberCV{},
182
+ &models.PhysicalAndHealthCV{},
183
+ &models.WorshipAndReligiousUnderstandingCV{},
184
+ &models.EducationCV{},
185
+ &models.JobCV{},
186
+ &models.AchievementCV{},
187
+ &models.MarriageReadinessProfile{},
188
+ &models.PartnerCriteria{},
189
+ )
190
+ if err != nil {
191
+ return fmt.Errorf("failed to auto-migrate models: %w", err)
192
+ }
193
+
194
+ // Create sequences
195
+ sequences := []string{
196
+ `CREATE SEQUENCE IF NOT EXISTS seq_ikh_counter START 1 INCREMENT 1 MINVALUE 1;`,
197
+ `CREATE SEQUENCE IF NOT EXISTS seq_akh_counter START 1 INCREMENT 1 MINVALUE 1;`,
198
+ }
199
+ for _, seq := range sequences {
200
+ if err := db.Exec(seq).Error; err != nil {
201
+ log.Printf("Failed to create sequence: %v", err)
202
+ return fmt.Errorf("failed to create sequence: %w", err)
203
+ }
204
+ }
205
+
206
+ log.Println("Auto-migration and sequence creation completed successfully")
207
+ return nil
208
+ }
209
+
210
+ // getValue retrieves an environment variable with a default value and type conversion.
211
  func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
212
  if value == "" {
213
  return defaultValue
214
  }
215
  convertedValue, err := convert(value)
216
  if err != nil {
217
+ log.Printf("Failed to convert %v to type %T: %v, using default: %v", value, defaultValue, err, defaultValue)
218
  return defaultValue
219
  }
220
  return convertedValue
221
  }
222
+
223
+ // stringConverter converts a string to a string (identity function).
224
+ func stringConverter(s string) (string, error) {
225
+ return s, nil
226
+ }
227
+
228
+ // intConverter converts a string to an int.
229
+ func intConverter(s string) (int, error) {
230
+ return strconv.Atoi(s)
231
+ }
232
+
233
+ // durationConverter converts a string to a time.Duration.
234
+ func durationConverter(s string) (time.Duration, error) {
235
+ return time.ParseDuration(s)
236
+ }
space/main.go CHANGED
@@ -27,31 +27,33 @@ import (
27
 
28
  func main() {
29
 
30
- // setup validation
 
 
 
 
31
  validator, err := validation.New(config.DB)
32
  utils.FatalIfErr("failed to setup validator", err)
33
 
34
- // setup storage
35
- localStorage := storage.NewLocalStorage("uploads", config.APP_URL+"/storage/")
36
-
37
- // setup email sender
38
- emailConfig := mail.Config{
39
- Host: config.SMTP_HOST,
40
- Port: config.SMTP_PORT,
41
- From: config.SMTP_SENDER_EMAIL,
42
- Username: config.SMTP_SENDER_EMAIL,
43
- Password: config.SMTP_SENDER_PASSWORD,
44
- }
45
-
46
- emailSender, err := mail.New(&emailConfig)
47
  utils.FatalIfErr("failed to setup email sender", err)
48
  mail.EmailSender = emailSender
49
 
50
- // setup redis task distributor and processor
51
  asynqRedisOpt := asynq.RedisClientOpt{
52
- Addr: net.JoinHostPort(config.REDIS_HOST, strconv.Itoa(config.REDIS_PORT)),
53
- Password: config.REDIS_PASSWORD,
54
- DB: config.REDIS_DB,
55
  }
56
 
57
  taskDistributor := worker.NewRedisTaskDistributor(asynqRedisOpt)
@@ -59,7 +61,6 @@ func main() {
59
  worker.AsyncTaskDistributor = taskDistributor
60
 
61
  // setup repo, service, and controller
62
-
63
  regionRepository := repositories.NewRegionRepository(config.DB)
64
  regionService := services.NewRegionService(regionRepository)
65
  regionController := region_controller.NewRegionController(regionService)
@@ -76,8 +77,8 @@ func main() {
76
  academyService := services.NewAcademyService(academyRepository, validator)
77
  academyController := academy_controller.NewAcademyController(academyService)
78
 
79
- quizRepository := repositories.NewQuizRepository(config.DB)
80
- quizService := services.NewQuizService(quizRepository, academyRepository, validator)
81
  quizController := quiz_controller.NewQuizController(quizService)
82
 
83
  cvRepository := repositories.NewCVRepository(config.DB)
@@ -111,7 +112,7 @@ func main() {
111
  utils.FatalIfErr("failed to create server", err)
112
 
113
  // run server
114
- slog.Info("Starting server", "address", config.TCP_ADDRESS, "port", config.HOST_PORT)
115
- err = s.Start(config.TCP_ADDRESS)
116
  utils.FatalIfErr("failed to start server", err)
117
  }
 
27
 
28
  func main() {
29
 
30
+ // load config
31
+ err := config.LoadConfig()
32
+ utils.FatalIfErr("failed to load config", err)
33
+
34
+ // setup validator for validation request
35
  validator, err := validation.New(config.DB)
36
  utils.FatalIfErr("failed to setup validator", err)
37
 
38
+ // setup storage for saving files
39
+ localStorage := storage.NewLocalStorage("uploads", config.GlobalConfig.App.AppURL+"/storage/")
40
+
41
+ // setup smtp for sending email
42
+ emailSender, err := mail.New(&mail.Config{
43
+ Host: config.GlobalConfig.SMTP.Host,
44
+ Port: config.GlobalConfig.SMTP.Port,
45
+ From: config.GlobalConfig.SMTP.SenderEmail,
46
+ Username: config.GlobalConfig.SMTP.SenderEmail,
47
+ Password: config.GlobalConfig.SMTP.Password,
48
+ })
 
 
49
  utils.FatalIfErr("failed to setup email sender", err)
50
  mail.EmailSender = emailSender
51
 
52
+ // setup task distributor and processor for async task
53
  asynqRedisOpt := asynq.RedisClientOpt{
54
+ Addr: net.JoinHostPort(config.GlobalConfig.Redis.Host, strconv.Itoa(config.GlobalConfig.Redis.Port)),
55
+ Password: config.GlobalConfig.Redis.Password,
56
+ DB: config.GlobalConfig.Redis.DB,
57
  }
58
 
59
  taskDistributor := worker.NewRedisTaskDistributor(asynqRedisOpt)
 
61
  worker.AsyncTaskDistributor = taskDistributor
62
 
63
  // setup repo, service, and controller
 
64
  regionRepository := repositories.NewRegionRepository(config.DB)
65
  regionService := services.NewRegionService(regionRepository)
66
  regionController := region_controller.NewRegionController(regionService)
 
77
  academyService := services.NewAcademyService(academyRepository, validator)
78
  academyController := academy_controller.NewAcademyController(academyService)
79
 
80
+ quizRepository := repositories.NewQuizRepository()
81
+ quizService := services.NewQuizService(config.DB, quizRepository, academyRepository, validator)
82
  quizController := quiz_controller.NewQuizController(quizService)
83
 
84
  cvRepository := repositories.NewCVRepository(config.DB)
 
112
  utils.FatalIfErr("failed to create server", err)
113
 
114
  // run server
115
+ slog.Info("Starting server", "address", config.GlobalConfig.App.HostAddress, "port", config.GlobalConfig.App.HostPort)
116
+ err = s.Start(config.GlobalConfig.App.TCPAddress)
117
  utils.FatalIfErr("failed to start server", err)
118
  }
space/models/exception_model.go CHANGED
@@ -20,6 +20,7 @@ type Exception struct {
20
 
21
  // quiz context
22
  QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
 
23
  QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
24
  QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
25
  AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
 
20
 
21
  // quiz context
22
  QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
23
+ QuizAlreadyPassed bool `json:"quiz_already_passed,omitempty"`
24
  QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
25
  QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
26
  AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
space/repositories/academy_repository.go CHANGED
@@ -26,7 +26,7 @@ type AcademyRepository interface {
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
- UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error)
30
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
31
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
32
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
@@ -288,34 +288,33 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
288
  return academies, pageInfo, nil
289
  }
290
 
291
- func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error) {
292
- var totalMaterial int64
293
- var totalReadMaterial int64
294
 
295
- // Hitung total materi dalam academy
296
- err := r.db.WithContext(ctx).
297
  Model(&models.AcademyMaterial{}).
298
  Where("academy_id = ?", academyID).
299
- Count(&totalMaterial).Error
300
  if err != nil {
301
  return 0, err
302
  }
303
 
304
- // Hitung total materi yang sudah dibaca oleh user
305
- err = r.db.WithContext(ctx).
 
 
 
 
306
  Model(&models.AcademyMaterialProgress{}).
307
- Joins("JOIN academy_materials am ON am.id = academy_materials_progress.academy_material_id").
308
- Where("academy_materials_progress.account_id = ? AND am.academy_id = ?", accountID, academyID).
309
- Count(&totalReadMaterial).Error
310
  if err != nil {
311
  return 0, err
312
  }
313
 
314
- // Hitung persentase
315
- if totalMaterial == 0 {
316
- return 0, nil
317
- }
318
- percentage := float64(totalReadMaterial) / float64(totalMaterial) * 100
319
  return percentage, nil
320
  }
321
 
 
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
+ UserGetPercentageProgressAcademyByID(ctx context.Context, db *gorm.DB, accountID, academyID int64) (float64, error)
30
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
31
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
32
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
 
288
  return academies, pageInfo, nil
289
  }
290
 
291
+ func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, db *gorm.DB, accountID, academyID int64) (float64, error) {
292
+ var totalMaterials, completedMaterials int64
 
293
 
294
+ // Count total materials for the academy
295
+ err := db.WithContext(ctx).
296
  Model(&models.AcademyMaterial{}).
297
  Where("academy_id = ?", academyID).
298
+ Count(&totalMaterials).Error
299
  if err != nil {
300
  return 0, err
301
  }
302
 
303
+ if totalMaterials == 0 {
304
+ return 100.0, nil // If no materials, consider progress as 100%
305
+ }
306
+
307
+ // Count completed materials for the account
308
+ err = db.WithContext(ctx).
309
  Model(&models.AcademyMaterialProgress{}).
310
+ Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
311
+ Count(&completedMaterials).Error
 
312
  if err != nil {
313
  return 0, err
314
  }
315
 
316
+ // Calculate percentage
317
+ percentage := (float64(completedMaterials) / float64(totalMaterials)) * 100
 
 
 
318
  return percentage, nil
319
  }
320
 
space/repositories/quiz_repository.go CHANGED
@@ -10,38 +10,37 @@ import (
10
  )
11
 
12
  type QuizRepository interface {
13
- UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
14
- UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
15
- UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
16
- UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
17
- UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
18
-
19
- UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error)
20
- UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
21
-
22
- UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
23
- UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error)
24
-
25
- UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
26
- UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
27
- UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
28
- UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error)
29
- UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
30
- UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error)
31
- UserSaveUserAnswer(ctx context.Context, answer *models.UserAnswer) error
32
- GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error)
33
- UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error
 
34
  }
35
 
36
- type quizRepository struct {
37
- db *gorm.DB
38
- }
39
 
40
- func NewQuizRepository(db *gorm.DB) QuizRepository {
41
- return &quizRepository{db: db}
42
  }
43
 
44
- func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
45
  var quizResponse models.UserGetQuizResponse
46
 
47
  rawQuery := `
@@ -73,7 +72,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
73
  q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
74
  `
75
 
76
- err := r.db.Debug().Raw(rawQuery, map[string]any{
77
  "accountID": req.AccountID,
78
  "academyID": req.AcademyID,
79
  }).Scan(&quizResponse).Error
@@ -88,10 +87,24 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
88
  return &quizResponse, nil
89
  }
90
 
91
- func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  var quizAttempt models.QuizAttempt
93
 
94
- err := r.db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
95
  if err != nil {
96
  return nil, err
97
  }
@@ -99,10 +112,10 @@ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID
99
  return &quizAttempt, nil
100
  }
101
 
102
- func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
103
  var totalAttempts int64
104
 
105
- err := r.db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
106
  if err != nil {
107
  return 0, err
108
  }
@@ -110,11 +123,11 @@ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID
110
  return totalAttempts, nil
111
  }
112
 
113
- func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
114
- return r.db.Debug().Create(attempt).Error
115
  }
116
 
117
- func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
118
  questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
119
 
120
  rawQuery := `
@@ -144,7 +157,7 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
144
  q.id;
145
  `
146
 
147
- err := r.db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
148
  if err != nil {
149
  return nil, err
150
  }
@@ -152,10 +165,10 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
152
  return questions, nil
153
  }
154
 
155
- func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error) {
156
  var quizAttempt models.QuizAttempt
157
 
158
- err := r.db.Where("id = ?", attemptID).
159
  Preload("Quiz").
160
  First(&quizAttempt).Error
161
  if err != nil {
@@ -165,14 +178,14 @@ func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64
165
  return &quizAttempt, nil
166
  }
167
 
168
- func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
169
- return r.db.Save(attempt).Error
170
  }
171
 
172
- func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
173
  var totalQuestion int64
174
 
175
- err := r.db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
176
  if err != nil {
177
  return 0, err
178
  }
@@ -180,10 +193,10 @@ func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID in
180
  return totalQuestion, nil
181
  }
182
 
183
- func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error) {
184
  var totalCorrectAnswer int64
185
 
186
- err := r.db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
187
  if err != nil {
188
  return 0, err
189
  }
@@ -191,11 +204,11 @@ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quiz
191
  return totalCorrectAnswer, nil
192
  }
193
 
194
- func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
195
- return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
196
  }
197
 
198
- func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error) {
199
  var remainingAttempts int64
200
 
201
  rawQuery := `
@@ -214,7 +227,7 @@ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID
214
  q.attempt_limit;
215
  `
216
 
217
- err := r.db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
218
  if err != nil {
219
  return 0, err
220
  }
@@ -222,7 +235,7 @@ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID
222
  return remainingAttempts, nil
223
  }
224
 
225
- func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
226
  var resultJSON string
227
  var response models.GetQuestionQuizResponse
228
 
@@ -251,14 +264,14 @@ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, que
251
  LEFT JOIN
252
  user_answers ua
253
  ON q.id = ua.question_id
254
- AND ua.quiz_attempt_id = $1
255
  WHERE
256
- q.id = $2
257
  GROUP BY
258
  q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
259
  `
260
 
261
- err := r.db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
262
  if err != nil {
263
  return nil, err
264
  }
@@ -271,10 +284,10 @@ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, que
271
  return &response, nil
272
  }
273
 
274
- func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int64) (*models.Question, error) {
275
  var question models.Question
276
 
277
- err := r.db.Where("id = ?", questionID).First(&question).Error
278
  if err != nil {
279
  return nil, err
280
  }
@@ -282,10 +295,10 @@ func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int
282
  return &question, nil
283
  }
284
 
285
- func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error) {
286
  var userAnswer models.UserAnswer
287
 
288
- err := r.db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
289
  if err != nil {
290
  return nil, err
291
  }
@@ -293,11 +306,11 @@ func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, quest
293
  return &userAnswer, nil
294
  }
295
 
296
- func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, userAnswer *models.UserAnswer) error {
297
- return r.db.Save(userAnswer).Error
298
  }
299
 
300
- func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
301
  var resultJSON string
302
  var reviewQuizResponse models.ReviewQuizResponse
303
 
@@ -326,7 +339,7 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
326
  FROM answers a
327
  WHERE a.question_id = q.id
328
  ),
329
- 'answer_id', ua.selected_answer_id
330
  ) ORDER BY q.order
331
  ),
332
  '[]'::json
@@ -341,7 +354,7 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
341
  q.quiz_id = ?
342
  `
343
 
344
- err := r.db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
345
  if err != nil {
346
  return nil, fmt.Errorf("failed to query reviews: %w", err)
347
  }
@@ -356,10 +369,10 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
356
  return &reviewQuizResponse, nil
357
  }
358
 
359
- func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error) {
360
  var attempt models.QuizAttempt
361
 
362
- err := r.db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
363
  if err != nil {
364
  return nil, err
365
  }
@@ -367,7 +380,7 @@ func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID in
367
  return &attempt, nil
368
  }
369
 
370
- func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error) {
371
  var correctOptionID int64
372
 
373
  rawQuery := `
@@ -378,7 +391,7 @@ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int6
378
  LIMIT 1;
379
  `
380
 
381
- err := r.db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
382
  if err != nil {
383
  return 0, err
384
  }
@@ -386,13 +399,20 @@ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int6
386
  return correctOptionID, nil
387
  }
388
 
389
- func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error {
390
- r.db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
391
- Delete(&models.AcademyMaterialProgress{})
 
 
 
 
392
 
393
- // Delete quiz attempts for the quiz associated with the academy
394
- r.db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
395
- Delete(&models.QuizAttempt{})
 
 
 
396
 
397
  return nil
398
  }
 
10
  )
11
 
12
  type QuizRepository interface {
13
+ UserGetQuiz(ctx context.Context, db *gorm.DB, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
14
+ UserIsPassedQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (bool, error)
15
+ UserGetActiveAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (*models.QuizAttempt, error)
16
+ UserGetTotalAttemptsQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error)
17
+ UserCreateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error
18
+ UserGetAttemptQuizQuestionsResponse(ctx context.Context, db *gorm.DB, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
19
+
20
+ UserGetAttemptByID(ctx context.Context, db *gorm.DB, attemptID int64) (*models.QuizAttempt, error)
21
+ UserUpdateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error
22
+
23
+ UserGetReviewQuiz(ctx context.Context, db *gorm.DB, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
24
+ UseGetLastAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) (*models.QuizAttempt, error)
25
+
26
+ UserGetTotalQuestionQuiz(ctx context.Context, db *gorm.DB, quizID int64) (int64, error)
27
+ UserGetTotalCorrectAnswerQuiz(ctx context.Context, db *gorm.DB, quizAttemptID int64) (int64, error)
28
+ UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) error
29
+ UserGetRemainingAttempts(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error)
30
+ UserGetQuestionQuiz(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
31
+ UserGetUserAnswer(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.UserAnswer, error)
32
+ UserSaveUserAnswer(ctx context.Context, db *gorm.DB, answer *models.UserAnswer) error
33
+ GetCorrectOptionID(ctx context.Context, db *gorm.DB, questionID int64) (int64, error)
34
+ UserDeleteProgressAndAttempt(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) error
35
  }
36
 
37
+ type quizRepository struct{}
 
 
38
 
39
+ func NewQuizRepository() QuizRepository {
40
+ return &quizRepository{}
41
  }
42
 
43
+ func (r *quizRepository) UserGetQuiz(ctx context.Context, db *gorm.DB, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
44
  var quizResponse models.UserGetQuizResponse
45
 
46
  rawQuery := `
 
72
  q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
73
  `
74
 
75
+ err := db.Debug().Raw(rawQuery, map[string]any{
76
  "accountID": req.AccountID,
77
  "academyID": req.AcademyID,
78
  }).Scan(&quizResponse).Error
 
87
  return &quizResponse, nil
88
  }
89
 
90
+ func (r *quizRepository) UserIsPassedQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (bool, error) {
91
+ var count int64
92
+
93
+ err := db.WithContext(ctx).
94
+ Model(&models.QuizAttempt{}).
95
+ Where("account_id = ? AND quiz_id = ? AND is_passed = ?", accountID, quizID, true).
96
+ Count(&count).Error
97
+ if err != nil {
98
+ return false, fmt.Errorf("failed to check if quiz is passed: %w", err)
99
+ }
100
+
101
+ return count > 0, nil
102
+ }
103
+
104
+ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (*models.QuizAttempt, error) {
105
  var quizAttempt models.QuizAttempt
106
 
107
+ err := db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
108
  if err != nil {
109
  return nil, err
110
  }
 
112
  return &quizAttempt, nil
113
  }
114
 
115
+ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error) {
116
  var totalAttempts int64
117
 
118
+ err := db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
119
  if err != nil {
120
  return 0, err
121
  }
 
123
  return totalAttempts, nil
124
  }
125
 
126
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error {
127
+ return db.Debug().Create(attempt).Error
128
  }
129
 
130
+ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, db *gorm.DB, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
131
  questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
132
 
133
  rawQuery := `
 
157
  q.id;
158
  `
159
 
160
+ err := db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
161
  if err != nil {
162
  return nil, err
163
  }
 
165
  return questions, nil
166
  }
167
 
168
+ func (r *quizRepository) UserGetAttemptByID(ctx context.Context, db *gorm.DB, attemptID int64) (*models.QuizAttempt, error) {
169
  var quizAttempt models.QuizAttempt
170
 
171
+ err := db.Where("id = ?", attemptID).
172
  Preload("Quiz").
173
  First(&quizAttempt).Error
174
  if err != nil {
 
178
  return &quizAttempt, nil
179
  }
180
 
181
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error {
182
+ return db.Save(attempt).Error
183
  }
184
 
185
+ func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, db *gorm.DB, quizID int64) (int64, error) {
186
  var totalQuestion int64
187
 
188
+ err := db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
189
  if err != nil {
190
  return 0, err
191
  }
 
193
  return totalQuestion, nil
194
  }
195
 
196
+ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, db *gorm.DB, quizAttemptID int64) (int64, error) {
197
  var totalCorrectAnswer int64
198
 
199
+ err := db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
200
  if err != nil {
201
  return 0, err
202
  }
 
204
  return totalCorrectAnswer, nil
205
  }
206
 
207
+ func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) error {
208
+ return db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
209
  }
210
 
211
+ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error) {
212
  var remainingAttempts int64
213
 
214
  rawQuery := `
 
227
  q.attempt_limit;
228
  `
229
 
230
+ err := db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
231
  if err != nil {
232
  return 0, err
233
  }
 
235
  return remainingAttempts, nil
236
  }
237
 
238
+ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
239
  var resultJSON string
240
  var response models.GetQuestionQuizResponse
241
 
 
264
  LEFT JOIN
265
  user_answers ua
266
  ON q.id = ua.question_id
267
+ AND ua.quiz_attempt_id = ?
268
  WHERE
269
+ q.id = ?
270
  GROUP BY
271
  q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
272
  `
273
 
274
+ err := db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
275
  if err != nil {
276
  return nil, err
277
  }
 
284
  return &response, nil
285
  }
286
 
287
+ func (r *quizRepository) UserGetQuestionByID(ctx context.Context, db *gorm.DB, questionID int64) (*models.Question, error) {
288
  var question models.Question
289
 
290
+ err := db.Where("id = ?", questionID).First(&question).Error
291
  if err != nil {
292
  return nil, err
293
  }
 
295
  return &question, nil
296
  }
297
 
298
+ func (r *quizRepository) UserGetUserAnswer(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.UserAnswer, error) {
299
  var userAnswer models.UserAnswer
300
 
301
+ err := db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
302
  if err != nil {
303
  return nil, err
304
  }
 
306
  return &userAnswer, nil
307
  }
308
 
309
+ func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, db *gorm.DB, userAnswer *models.UserAnswer) error {
310
+ return db.Save(userAnswer).Error
311
  }
312
 
313
+ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, db *gorm.DB, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
314
  var resultJSON string
315
  var reviewQuizResponse models.ReviewQuizResponse
316
 
 
339
  FROM answers a
340
  WHERE a.question_id = q.id
341
  ),
342
+ 'answer_id', NULLIF(ua.selected_answer_id, 0)
343
  ) ORDER BY q.order
344
  ),
345
  '[]'::json
 
354
  q.quiz_id = ?
355
  `
356
 
357
+ err := db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
358
  if err != nil {
359
  return nil, fmt.Errorf("failed to query reviews: %w", err)
360
  }
 
369
  return &reviewQuizResponse, nil
370
  }
371
 
372
+ func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) (*models.QuizAttempt, error) {
373
  var attempt models.QuizAttempt
374
 
375
+ err := db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
376
  if err != nil {
377
  return nil, err
378
  }
 
380
  return &attempt, nil
381
  }
382
 
383
+ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, db *gorm.DB, questionID int64) (int64, error) {
384
  var correctOptionID int64
385
 
386
  rawQuery := `
 
391
  LIMIT 1;
392
  `
393
 
394
+ err := db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
395
  if err != nil {
396
  return 0, err
397
  }
 
399
  return correctOptionID, nil
400
  }
401
 
402
+ func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) error {
403
+ // Delete academy material progress
404
+ err := db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
405
+ Delete(&models.AcademyMaterialProgress{}).Error
406
+ if err != nil {
407
+ return err
408
+ }
409
 
410
+ // Delete quiz attempts
411
+ err = db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
412
+ Delete(&models.QuizAttempt{}).Error
413
+ if err != nil {
414
+ return err
415
+ }
416
 
417
  return nil
418
  }
space/response/api_response_v2.go CHANGED
@@ -4,6 +4,7 @@ import (
4
  "encoding/json"
5
  "errors"
6
  "net/http"
 
7
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
@@ -49,7 +50,8 @@ func HandleError(c *gin.Context, err error) {
49
  responseError(c, http.StatusBadRequest, exception)
50
  case exception.AcademyNotFinished:
51
  responseError(c, http.StatusBadRequest, exception)
52
-
 
53
  case exception.ValidationError:
54
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
55
  default:
@@ -84,6 +86,16 @@ func HandleError(c *gin.Context, err error) {
84
  return
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
87
  utils.LogError(err)
88
  responseError(c, http.StatusInternalServerError, models.Exception{
89
  InternalServerError: true,
 
4
  "encoding/json"
5
  "errors"
6
  "net/http"
7
+ "strconv"
8
 
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/validation"
 
50
  responseError(c, http.StatusBadRequest, exception)
51
  case exception.AcademyNotFinished:
52
  responseError(c, http.StatusBadRequest, exception)
53
+ case exception.QuizAlreadyPassed:
54
+ responseError(c, http.StatusBadRequest, exception)
55
  case exception.ValidationError:
56
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
57
  default:
 
86
  return
87
  }
88
 
89
+ var numErr *strconv.NumError
90
+ if errors.As(err, &numErr) {
91
+ responseError(c, http.StatusBadRequest, models.Exception{
92
+ BadRequest: true,
93
+ Message: "Invalid number format",
94
+ Err: err,
95
+ })
96
+ return
97
+ }
98
+
99
  utils.LogError(err)
100
  responseError(c, http.StatusInternalServerError, models.Exception{
101
  InternalServerError: true,
space/services/email_service.go CHANGED
@@ -44,7 +44,7 @@ func (s *emailService) CreateEmailVerification(ctx context.Context, req *models.
44
  }
45
  }
46
 
47
- remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Minute
48
  dueTime := time.Now().Add(remainingTime)
49
 
50
  payload := models.EmailVerification{
 
44
  }
45
  }
46
 
47
+ remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
48
  dueTime := time.Now().Add(remainingTime)
49
 
50
  payload := models.EmailVerification{
space/services/forgot_password_service.go CHANGED
@@ -1,13 +1,14 @@
1
  package services
2
 
3
  import (
4
- "api.qobiltu.id/pkg/worker"
5
- "api.qobiltu.id/utils"
6
  "context"
7
- "github.com/hibiken/asynq"
8
  "strconv"
9
  "time"
10
 
 
 
 
 
11
  "api.qobiltu.id/config"
12
  "api.qobiltu.id/models"
13
  "api.qobiltu.id/repositories"
@@ -40,7 +41,7 @@ func (s *ForgotPasswordService) Create(email string) {
40
  return
41
  }
42
 
43
- remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
44
  dueTime := CalculateDueTime(remainingTime)
45
 
46
  s.Constructor.UUID = uuid.NewV4()
 
1
  package services
2
 
3
  import (
 
 
4
  "context"
 
5
  "strconv"
6
  "time"
7
 
8
+ "api.qobiltu.id/pkg/worker"
9
+ "api.qobiltu.id/utils"
10
+ "github.com/hibiken/asynq"
11
+
12
  "api.qobiltu.id/config"
13
  "api.qobiltu.id/models"
14
  "api.qobiltu.id/repositories"
 
41
  return
42
  }
43
 
44
+ remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
45
  dueTime := CalculateDueTime(remainingTime)
46
 
47
  s.Constructor.UUID = uuid.NewV4()
space/services/jwt_service.go CHANGED
@@ -11,16 +11,15 @@ import (
11
  "golang.org/x/crypto/bcrypt"
12
  )
13
 
14
- var salt = config.Salt
15
- var secretKey = []byte(salt)
16
-
17
  func GenerateToken(user *models.Account) (string, error) {
 
 
18
  claims := models.CustomClaims{
19
  UserID: user.Id,
20
  RegisteredClaims: jwt.RegisteredClaims{
21
  ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
22
  IssuedAt: jwt.NewNumericDate(time.Now()),
23
- Issuer: "apqobiltu.id",
24
  },
25
  }
26
 
@@ -38,13 +37,11 @@ func ExtractBearerToken(authHeader string) (string, error) {
38
  }
39
 
40
  func VerifyToken(bearerToken string) (uint, string, error) {
41
- // fmt.Println("bearerToken :", bearerToken)
42
 
43
  tokenData, err := ExtractBearerToken(bearerToken)
44
  if err != nil {
45
  return 0, "invalid-token", err
46
- } else {
47
- // fmt.Println("Extracted Token:", tokenData)
48
  }
49
 
50
  token, err := jwt.ParseWithClaims(tokenData, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
@@ -55,7 +52,6 @@ func VerifyToken(bearerToken string) (uint, string, error) {
55
  return 0, "invalid-token", err
56
  }
57
 
58
- // Extract the claims
59
  claims, ok := token.Claims.(*models.CustomClaims)
60
  if !ok || !token.Valid {
61
  return 0, "invalid-token", err
@@ -74,6 +70,7 @@ func VerifyPassword(hashedPassword, password string) error {
74
  }
75
  return nil
76
  }
 
77
  func HashPassword(password string) (string, error) {
78
  bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
79
  return string(bytes), err
 
11
  "golang.org/x/crypto/bcrypt"
12
  )
13
 
 
 
 
14
  func GenerateToken(user *models.Account) (string, error) {
15
+ secretKey := []byte(config.GlobalConfig.App.Salt)
16
+
17
  claims := models.CustomClaims{
18
  UserID: user.Id,
19
  RegisteredClaims: jwt.RegisteredClaims{
20
  ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
21
  IssuedAt: jwt.NewNumericDate(time.Now()),
22
+ Issuer: "api.qobiltu.id",
23
  },
24
  }
25
 
 
37
  }
38
 
39
  func VerifyToken(bearerToken string) (uint, string, error) {
40
+ secretKey := []byte(config.GlobalConfig.App.Salt)
41
 
42
  tokenData, err := ExtractBearerToken(bearerToken)
43
  if err != nil {
44
  return 0, "invalid-token", err
 
 
45
  }
46
 
47
  token, err := jwt.ParseWithClaims(tokenData, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
 
52
  return 0, "invalid-token", err
53
  }
54
 
 
55
  claims, ok := token.Claims.(*models.CustomClaims)
56
  if !ok || !token.Valid {
57
  return 0, "invalid-token", err
 
70
  }
71
  return nil
72
  }
73
+
74
  func HashPassword(password string) (string, error) {
75
  bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
76
  return string(bytes), err
space/services/quiz_service.go CHANGED
@@ -5,6 +5,7 @@ import (
5
  "errors"
6
  "time"
7
 
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
10
  "api.qobiltu.id/repositories"
@@ -27,17 +28,23 @@ type QuizService interface {
27
  }
28
 
29
  type quizService struct {
 
30
  quizRepository repositories.QuizRepository
31
  academyRepository repositories.AcademyRepository
32
  validator *validation.Validator
33
  }
34
 
35
- func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
36
- return &quizService{quizRepository: quizRepository, academyRepository: academyRepository, validator: validator}
 
 
 
 
 
37
  }
38
 
39
  func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
40
- quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
41
  if err != nil {
42
  return nil, response.HandleGormError(err, "Internal Server Error")
43
  }
@@ -46,8 +53,8 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
46
  }
47
 
48
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
49
- // ambil data quiz attempt
50
- quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
51
  AccountID: req.AccountID,
52
  AcademyID: req.AcademyID,
53
  })
@@ -57,31 +64,41 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
57
 
58
  quiz := &quizAttempt.Quiz
59
 
60
- // ambil attempt quiz yg sedang aktif
61
- existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
 
 
 
 
 
 
 
 
 
 
62
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
63
  return nil, response.HandleGormError(err, "Internal Server Error")
64
  }
65
 
66
- // jika ada attempt yang sedang active
67
  if existingAttempt != nil {
68
  return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
69
  }
70
 
 
71
  return s.handleNewAttempt(ctx, req, quiz)
72
  }
73
 
74
  func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
75
  now := time.Now()
76
 
77
- // jika attempt nya telah melewati batas waktu dan belum submit / finish
78
  if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
79
- // frontend nge-trigger submit
80
  return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
81
  }
82
 
83
- // jika masih dalam waktu yang ditentukan
84
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
85
  if err != nil {
86
  return nil, response.HandleGormError(err, "Internal Server Error")
87
  }
@@ -101,64 +118,77 @@ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserA
101
  }
102
 
103
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
 
104
 
105
- // ini sekedar untuk make sure apakah masih bisa attemp dan telah membaca semua materi.
106
- totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
107
- if err != nil {
108
- return nil, response.HandleGormError(err, "Internal Server Error")
109
- }
110
- if totalAttempts >= quiz.AttemptLimit {
111
- return nil, models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
112
- }
 
113
 
114
- percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
115
- if err != nil {
116
- return nil, response.HandleGormError(err, "Internal Server Error")
117
- }
118
- if percentage < 100 {
119
- return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
120
- }
 
121
 
122
- // buat attempt quiz
123
- attempt := models.QuizAttempt{
124
- AccountID: req.AccountID,
125
- QuizID: quiz.ID,
126
- AcademyID: quiz.AcademyID,
127
- StartedAt: time.Now(),
128
- DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
129
- }
130
 
131
- if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &attempt); err != nil {
132
- return nil, response.HandleGormError(err, "Internal Server Error")
133
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- // ambil question
136
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
137
  if err != nil {
138
- return nil, response.HandleGormError(err, "Internal Server Error")
139
  }
140
 
141
- questions = utils.ShuffleWithKey(questions, attempt.ID)
142
-
143
- return &models.UserAttemptQuizResponse{
144
- ID: attempt.ID,
145
- AccountID: attempt.AccountID,
146
- QuizID: attempt.QuizID,
147
- StartedAt: attempt.StartedAt,
148
- DueAt: attempt.DueAt,
149
- FinishedAt: attempt.FinishedAt,
150
- Score: attempt.Score,
151
- Questions: questions,
152
- }, nil
153
  }
154
 
155
  func (s *quizService) calculateQuizScore(
156
  ctx context.Context,
 
157
  quiz *models.Quiz,
158
  attempt *models.QuizAttempt,
159
  ) error {
160
- // ambil total question dari quiz
161
- totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
162
  if err != nil {
163
  return err
164
  }
@@ -172,14 +202,14 @@ func (s *quizService) calculateQuizScore(
172
  return nil
173
  }
174
 
175
- // ambil semua user answer yang is_correct nya true
176
- correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
177
  if err != nil {
178
  return err
179
  }
180
 
 
181
  score := float64(correctAnswer) / float64(totalQuestion) * 100
182
-
183
  attempt.Score = score
184
  attempt.TotalQuestions = totalQuestion
185
  attempt.TotalCorrectAnswer = correctAnswer
@@ -193,14 +223,14 @@ func (s *quizService) calculateQuizScore(
193
  }
194
 
195
  func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
196
- // pastiin dulu attemp nya ada atau tidak
197
- _, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
198
  if err != nil {
199
  return nil, response.HandleGormError(err, "Internal Server Error")
200
  }
201
 
202
- // ambil pertanyaan dan jawaban pengguna
203
- question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
204
  if err != nil {
205
  if errors.Is(err, gorm.ErrRecordNotFound) {
206
  return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
@@ -212,105 +242,135 @@ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQu
212
  }
213
 
214
  func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
 
215
 
216
- correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, req.QuestionID)
217
- if err != nil {
218
- return nil, response.HandleGormError(err, "Internal Server Error")
219
- }
220
-
221
- question, err := s.quizRepository.UserGetUserAnswer(ctx, req.AttemptID, req.QuestionID)
222
- if err != nil {
223
-
224
- // jika belum ada answer
225
- if errors.Is(err, gorm.ErrRecordNotFound) {
226
- userAnswer := models.UserAnswer{
227
- QuizAttemptID: req.AttemptID,
228
- QuestionID: req.QuestionID,
229
- AnswerID: req.AnswerID,
230
- IsDoubt: req.IsDoubt,
231
- IsCorrect: false,
232
- }
233
-
234
- if req.AnswerID != nil {
235
- userAnswer.IsCorrect = *req.AnswerID == correctOptionID
236
- }
237
-
238
- if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
239
- return nil, response.HandleGormError(err, "Internal Server Error")
240
- }
241
 
242
- res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
243
- if err != nil {
244
- return nil, response.HandleGormError(err, "Internal Server Error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
246
 
247
- return res, nil
248
  }
249
 
250
- return nil, response.HandleGormError(err, "Internal Server Error")
251
- }
 
 
 
 
252
 
253
- // updating answer
254
- question.AnswerID = req.AnswerID
255
- question.IsDoubt = req.IsDoubt
256
 
257
- if req.AnswerID != nil {
258
- question.IsCorrect = *req.AnswerID == correctOptionID
259
- }
 
 
 
 
260
 
261
- if err := s.quizRepository.UserSaveUserAnswer(ctx, question); err != nil {
262
- return nil, response.HandleGormError(err, "Internal Server Error")
263
- }
264
 
265
- res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
266
  if err != nil {
267
- if errors.Is(err, gorm.ErrRecordNotFound) {
268
- return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
269
- }
270
- return nil, response.HandleGormError(err, "Internal Server Error")
271
  }
272
 
273
- return res, nil
274
  }
275
 
276
  func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
277
- attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
278
- if err != nil {
279
- return nil, response.HandleGormError(err, "Internal Server Error")
280
- }
281
 
282
- if err := s.calculateQuizScore(ctx, attempt.Quiz, attempt); err != nil {
283
- return nil, response.HandleGormError(err, "Internal Server Error")
284
- }
 
 
 
285
 
286
- if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
287
- return nil, response.HandleGormError(err, "Internal Server Error")
288
- }
 
289
 
290
- remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
291
- if err != nil {
292
- return nil, response.HandleGormError(err, "Internal Server Error")
293
- }
294
 
295
- if remainingAttempts == 0 {
296
- // hapus progress dan semua attempt pada quiz
297
- if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
298
- return nil, response.HandleGormError(err, "Internal Server Error")
299
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  }
301
 
302
- return &models.SubmitQuizResponse{
303
- QuizAttempt: *attempt,
304
- RemainingAttempts: remainingAttempts,
305
- }, nil
306
  }
307
 
308
  func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
309
- attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
 
310
  if err != nil {
311
  return nil, response.HandleGormError(err, "Internal Server Error")
312
  }
313
- remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
 
 
314
  if err != nil {
315
  return nil, response.HandleGormError(err, "Internal Server Error")
316
  }
@@ -322,12 +382,14 @@ func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuiz
322
  }
323
 
324
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
325
- attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
 
326
  if err != nil {
327
  return nil, response.HandleGormError(err, "Internal Server Error")
328
  }
329
 
330
- review, err := s.quizRepository.UserGetReviewQuiz(ctx, attempt.ID, attempt.AccountID, attempt.QuizID)
 
331
  if err != nil {
332
  return nil, response.HandleGormError(err, "Internal Server Error")
333
  }
 
5
  "errors"
6
  "time"
7
 
8
+ "api.qobiltu.id/config"
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/validation"
11
  "api.qobiltu.id/repositories"
 
28
  }
29
 
30
  type quizService struct {
31
+ db *gorm.DB
32
  quizRepository repositories.QuizRepository
33
  academyRepository repositories.AcademyRepository
34
  validator *validation.Validator
35
  }
36
 
37
+ func NewQuizService(db *gorm.DB, quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
38
+ return &quizService{
39
+ db: db,
40
+ quizRepository: quizRepository,
41
+ academyRepository: academyRepository,
42
+ validator: validator,
43
+ }
44
  }
45
 
46
  func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
47
+ quiz, err := s.quizRepository.UserGetQuiz(ctx, s.db, req)
48
  if err != nil {
49
  return nil, response.HandleGormError(err, "Internal Server Error")
50
  }
 
53
  }
54
 
55
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
56
+ // Fetch quiz data
57
+ quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, s.db, &models.UserGetQuizRequest{
58
  AccountID: req.AccountID,
59
  AcademyID: req.AcademyID,
60
  })
 
64
 
65
  quiz := &quizAttempt.Quiz
66
 
67
+ // check is already passed
68
+ isPassedQuiz, err := s.quizRepository.UserIsPassedQuiz(ctx, s.db, req.AccountID, quiz.ID)
69
+ if err != nil {
70
+ return nil, response.HandleGormError(err, "Internal Server Error")
71
+ }
72
+
73
+ if isPassedQuiz {
74
+ return nil, models.Exception{QuizAlreadyPassed: true, Message: "You have already passed this quiz"}
75
+ }
76
+
77
+ // Check for active attempt
78
+ existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, s.db, req.AccountID, quiz.ID)
79
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
80
  return nil, response.HandleGormError(err, "Internal Server Error")
81
  }
82
 
83
+ // Handle existing active attempt
84
  if existingAttempt != nil {
85
  return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
86
  }
87
 
88
+ // Handle new attempt
89
  return s.handleNewAttempt(ctx, req, quiz)
90
  }
91
 
92
  func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
93
  now := time.Now()
94
 
95
+ // Check if attempt has expired
96
  if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
 
97
  return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
98
  }
99
 
100
+ // Fetch questions for active attempt
101
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, s.db, req.AccountID, quiz.ID, attempt.ID)
102
  if err != nil {
103
  return nil, response.HandleGormError(err, "Internal Server Error")
104
  }
 
118
  }
119
 
120
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
121
+ var result *models.UserAttemptQuizResponse
122
 
123
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
124
+ // Check total attempts
125
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, tx, req.AccountID, quiz.ID)
126
+ if err != nil {
127
+ return response.HandleGormError(err, "Internal Server Error")
128
+ }
129
+ if totalAttempts >= quiz.AttemptLimit {
130
+ return models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
131
+ }
132
 
133
+ // Check academy progress
134
+ percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, tx, req.AccountID, req.AcademyID)
135
+ if err != nil {
136
+ return response.HandleGormError(err, "Internal Server Error")
137
+ }
138
+ if percentage < 100 {
139
+ return models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
140
+ }
141
 
142
+ // Create new attempt
143
+ attempt := models.QuizAttempt{
144
+ AccountID: req.AccountID,
145
+ QuizID: quiz.ID,
146
+ AcademyID: quiz.AcademyID,
147
+ StartedAt: time.Now(),
148
+ DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
149
+ }
150
 
151
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, tx, &attempt); err != nil {
152
+ return response.HandleGormError(err, "Internal Server Error")
153
+ }
154
+
155
+ // Fetch questions
156
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, tx, req.AccountID, quiz.ID, attempt.ID)
157
+ if err != nil {
158
+ return response.HandleGormError(err, "Internal Server Error")
159
+ }
160
+
161
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
162
+
163
+ result = &models.UserAttemptQuizResponse{
164
+ ID: attempt.ID,
165
+ AccountID: attempt.AccountID,
166
+ QuizID: attempt.QuizID,
167
+ StartedAt: attempt.StartedAt,
168
+ DueAt: attempt.DueAt,
169
+ FinishedAt: attempt.FinishedAt,
170
+ Score: attempt.Score,
171
+ Questions: questions,
172
+ }
173
+
174
+ return nil
175
+ })
176
 
 
 
177
  if err != nil {
178
+ return nil, models.Exception{InternalServerError: true, Message: "Internal Server Error"}
179
  }
180
 
181
+ return result, nil
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
  func (s *quizService) calculateQuizScore(
185
  ctx context.Context,
186
+ tx *gorm.DB,
187
  quiz *models.Quiz,
188
  attempt *models.QuizAttempt,
189
  ) error {
190
+ // Fetch total questions
191
+ totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, tx, attempt.QuizID)
192
  if err != nil {
193
  return err
194
  }
 
202
  return nil
203
  }
204
 
205
+ // Fetch total correct answers
206
+ correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, tx, attempt.ID)
207
  if err != nil {
208
  return err
209
  }
210
 
211
+ // Calculate score
212
  score := float64(correctAnswer) / float64(totalQuestion) * 100
 
213
  attempt.Score = score
214
  attempt.TotalQuestions = totalQuestion
215
  attempt.TotalCorrectAnswer = correctAnswer
 
223
  }
224
 
225
  func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
226
+ // Check if attempt exists
227
+ _, err := s.quizRepository.UserGetAttemptByID(ctx, s.db, req.AttemptID)
228
  if err != nil {
229
  return nil, response.HandleGormError(err, "Internal Server Error")
230
  }
231
 
232
+ // Fetch question and user answer
233
+ question, err := s.quizRepository.UserGetQuestionQuiz(ctx, s.db, req.AttemptID, req.QuestionID)
234
  if err != nil {
235
  if errors.Is(err, gorm.ErrRecordNotFound) {
236
  return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
 
242
  }
243
 
244
  func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
245
+ var result *models.GetQuestionQuizResponse
246
 
247
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
248
+ // Fetch correct option ID
249
+ correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, tx, req.QuestionID)
250
+ if err != nil {
251
+ return response.HandleGormError(err, "Internal Server Error")
252
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ // Check if user answer exists
255
+ question, err := s.quizRepository.UserGetUserAnswer(ctx, tx, req.AttemptID, req.QuestionID)
256
+ if err != nil {
257
+ // If no answer exists, create a new one
258
+ if errors.Is(err, gorm.ErrRecordNotFound) {
259
+ userAnswer := models.UserAnswer{
260
+ QuizAttemptID: req.AttemptID,
261
+ QuestionID: req.QuestionID,
262
+ AnswerID: req.AnswerID,
263
+ IsDoubt: req.IsDoubt,
264
+ IsCorrect: false,
265
+ }
266
+
267
+ if req.AnswerID != nil {
268
+ userAnswer.IsCorrect = *req.AnswerID == correctOptionID
269
+ }
270
+
271
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, &userAnswer); err != nil {
272
+ return response.HandleGormError(err, "Internal Server Error")
273
+ }
274
+
275
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
276
+ if err != nil {
277
+ return response.HandleGormError(err, "Internal Server Error")
278
+ }
279
+
280
+ result = res
281
+ return nil
282
  }
283
 
284
+ return response.HandleGormError(err, "Internal Server Error")
285
  }
286
 
287
+ // Update existing answer
288
+ question.AnswerID = req.AnswerID
289
+ question.IsDoubt = req.IsDoubt
290
+ if req.AnswerID != nil {
291
+ question.IsCorrect = *req.AnswerID == correctOptionID
292
+ }
293
 
294
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, question); err != nil {
295
+ return response.HandleGormError(err, "Internal Server Error")
296
+ }
297
 
298
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
299
+ if err != nil {
300
+ if errors.Is(err, gorm.ErrRecordNotFound) {
301
+ return models.Exception{DataNotFound: true, Message: "Question not found"}
302
+ }
303
+ return response.HandleGormError(err, "Internal Server Error")
304
+ }
305
 
306
+ result = res
307
+ return nil
308
+ })
309
 
 
310
  if err != nil {
311
+ return nil, err
 
 
 
312
  }
313
 
314
+ return result, nil
315
  }
316
 
317
  func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
318
+ var result *models.SubmitQuizResponse
 
 
 
319
 
320
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
321
+ // Fetch attempt
322
+ attempt, err := s.quizRepository.UserGetAttemptByID(ctx, tx, req.AttemptID)
323
+ if err != nil {
324
+ return response.HandleGormError(err, "Internal Server Error")
325
+ }
326
 
327
+ // Calculate score
328
+ if err := s.calculateQuizScore(ctx, tx, attempt.Quiz, attempt); err != nil {
329
+ return response.HandleGormError(err, "Internal Server Error")
330
+ }
331
 
332
+ // Update attempt
333
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, tx, attempt); err != nil {
334
+ return response.HandleGormError(err, "Internal Server Error")
335
+ }
336
 
337
+ // Get remaining attempts
338
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, tx, attempt.AccountID, attempt.AcademyID)
339
+ if err != nil {
340
+ return response.HandleGormError(err, "Internal Server Error")
341
  }
342
+
343
+ // If no remaining attempts, delete progress and attempts
344
+ if remainingAttempts == 0 {
345
+ if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, tx, attempt.AccountID, attempt.AcademyID); err != nil {
346
+ return response.HandleGormError(err, "Internal Server Error")
347
+ }
348
+ }
349
+
350
+ result = &models.SubmitQuizResponse{
351
+ QuizAttempt: *attempt,
352
+ RemainingAttempts: remainingAttempts,
353
+ }
354
+
355
+ return nil
356
+ })
357
+
358
+ if err != nil {
359
+ return nil, err
360
  }
361
 
362
+ return result, nil
 
 
 
363
  }
364
 
365
  func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
366
+ // Fetch last attempt
367
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
368
  if err != nil {
369
  return nil, response.HandleGormError(err, "Internal Server Error")
370
  }
371
+
372
+ // Get remaining attempts
373
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, s.db, attempt.AccountID, attempt.AcademyID)
374
  if err != nil {
375
  return nil, response.HandleGormError(err, "Internal Server Error")
376
  }
 
382
  }
383
 
384
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
385
+ // Fetch last attempt
386
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
387
  if err != nil {
388
  return nil, response.HandleGormError(err, "Internal Server Error")
389
  }
390
 
391
+ // Fetch review
392
+ review, err := s.quizRepository.UserGetReviewQuiz(ctx, s.db, attempt.ID, attempt.AccountID, attempt.QuizID)
393
  if err != nil {
394
  return nil, response.HandleGormError(err, "Internal Server Error")
395
  }
space/space/controller/quiz/quiz_controller.go CHANGED
@@ -20,6 +20,7 @@ type QuizController interface {
20
  UserGetQuestionQuiz(ctx *gin.Context)
21
  UserAnswerQuiz(ctx *gin.Context)
22
  UserSubmitQuiz(ctx *gin.Context)
 
23
  UserReviewQuiz(ctx *gin.Context)
24
  }
25
 
@@ -189,6 +190,29 @@ func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
189
  response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
190
  }
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
193
  academyID := ctx.Query("academy_id")
194
  academyIDInt, err := strconv.Atoi(academyID)
 
20
  UserGetQuestionQuiz(ctx *gin.Context)
21
  UserAnswerQuiz(ctx *gin.Context)
22
  UserSubmitQuiz(ctx *gin.Context)
23
+ UserResultQuiz(ctx *gin.Context)
24
  UserReviewQuiz(ctx *gin.Context)
25
  }
26
 
 
190
  response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
191
  }
192
 
193
+ func (c *quizController) UserResultQuiz(ctx *gin.Context) {
194
+ academyID := ctx.Query("academy_id")
195
+ academyIDInt, err := strconv.Atoi(academyID)
196
+ if err != nil {
197
+ response.HandleError(ctx, err)
198
+ return
199
+ }
200
+
201
+ accountData := middleware.GetAccountData(ctx)
202
+ req := models.ResultQuizRequest{
203
+ AccountID: int64(accountData.UserID),
204
+ AcademyID: int64(academyIDInt),
205
+ }
206
+
207
+ res, err := c.quizService.UserResultQuiz(ctx, &req)
208
+ if err != nil {
209
+ response.HandleError(ctx, err)
210
+ return
211
+ }
212
+
213
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
214
+ }
215
+
216
  func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
217
  academyID := ctx.Query("academy_id")
218
  academyIDInt, err := strconv.Atoi(academyID)
space/space/models/request_model.go CHANGED
@@ -234,6 +234,17 @@ type (
234
  RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
 
237
  // REVIEW QUIZ
238
  ReviewQuizRequest struct {
239
  AccountID int64 `json:"account_id" validate:"required"`
@@ -254,7 +265,7 @@ type (
254
  Content string `json:"content"`
255
  IsCorrect bool `json:"is_correct"`
256
  } `json:"answer_options"`
257
- AnswerID int64 `json:"answer_id"`
258
  }
259
 
260
  ReviewQuizResponse struct {
 
234
  RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
  }
236
 
237
+ // RESULT QUIZ
238
+ ResultQuizRequest struct {
239
+ AccountID int64 `json:"account_id" validate:"required"`
240
+ AcademyID int64 `json:"academy_id" validate:"required"`
241
+ }
242
+
243
+ ResultQuizResponse struct {
244
+ QuizAttempt
245
+ RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
246
+ }
247
+
248
  // REVIEW QUIZ
249
  ReviewQuizRequest struct {
250
  AccountID int64 `json:"account_id" validate:"required"`
 
265
  Content string `json:"content"`
266
  IsCorrect bool `json:"is_correct"`
267
  } `json:"answer_options"`
268
+ AnswerID *int64 `json:"answer_id"`
269
  }
270
 
271
  ReviewQuizResponse struct {
space/space/router/quiz_route.go CHANGED
@@ -12,6 +12,7 @@ func (s *Server) QuizRoute() {
12
  userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
  userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
  userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
 
15
  userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
16
  }
17
  }
 
12
  userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
  userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
  userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
15
+ userRouterGroup.GET("/result", middleware.AuthUser, s.quizController.UserResultQuiz)
16
  userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
17
  }
18
  }
space/space/services/quiz_service.go CHANGED
@@ -5,6 +5,7 @@ import (
5
  "errors"
6
  "time"
7
 
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
10
  "api.qobiltu.id/repositories"
@@ -22,21 +23,28 @@ type QuizService interface {
22
  UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
23
  UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
24
  UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
 
25
  UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
  }
27
 
28
  type quizService struct {
 
29
  quizRepository repositories.QuizRepository
30
  academyRepository repositories.AcademyRepository
31
  validator *validation.Validator
32
  }
33
 
34
- func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
35
- return &quizService{quizRepository: quizRepository, academyRepository: academyRepository, validator: validator}
 
 
 
 
 
36
  }
37
 
38
  func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
39
- quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
40
  if err != nil {
41
  return nil, response.HandleGormError(err, "Internal Server Error")
42
  }
@@ -45,8 +53,8 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
45
  }
46
 
47
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
48
- // ambil data quiz attempt
49
- quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
50
  AccountID: req.AccountID,
51
  AcademyID: req.AcademyID,
52
  })
@@ -56,31 +64,41 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
56
 
57
  quiz := &quizAttempt.Quiz
58
 
59
- // ambil attempt quiz yg sedang aktif
60
- existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
 
 
 
 
 
 
 
 
 
 
61
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
62
  return nil, response.HandleGormError(err, "Internal Server Error")
63
  }
64
 
65
- // jika ada attempt yang sedang active
66
  if existingAttempt != nil {
67
  return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
68
  }
69
 
 
70
  return s.handleNewAttempt(ctx, req, quiz)
71
  }
72
 
73
  func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
  now := time.Now()
75
 
76
- // jika attempt nya telah melewati batas waktu dan belum submit / finish
77
  if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
78
- // frontend nge-trigger submit
79
  return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
80
  }
81
 
82
- // jika masih dalam waktu yang ditentukan
83
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
84
  if err != nil {
85
  return nil, response.HandleGormError(err, "Internal Server Error")
86
  }
@@ -100,64 +118,77 @@ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserA
100
  }
101
 
102
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
 
103
 
104
- // ini sekedar untuk make sure apakah masih bisa attemp dan telah membaca semua materi.
105
- totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
106
- if err != nil {
107
- return nil, response.HandleGormError(err, "Internal Server Error")
108
- }
109
- if totalAttempts >= quiz.AttemptLimit {
110
- return nil, models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
111
- }
 
112
 
113
- percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
114
- if err != nil {
115
- return nil, response.HandleGormError(err, "Internal Server Error")
116
- }
117
- if percentage < 100 {
118
- return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
119
- }
 
120
 
121
- // buat attempt quiz
122
- attempt := models.QuizAttempt{
123
- AccountID: req.AccountID,
124
- QuizID: quiz.ID,
125
- AcademyID: quiz.AcademyID,
126
- StartedAt: time.Now(),
127
- DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
128
- }
129
 
130
- if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &attempt); err != nil {
131
- return nil, response.HandleGormError(err, "Internal Server Error")
132
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- // ambil question
135
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
136
  if err != nil {
137
- return nil, response.HandleGormError(err, "Internal Server Error")
138
  }
139
 
140
- questions = utils.ShuffleWithKey(questions, attempt.ID)
141
-
142
- return &models.UserAttemptQuizResponse{
143
- ID: attempt.ID,
144
- AccountID: attempt.AccountID,
145
- QuizID: attempt.QuizID,
146
- StartedAt: attempt.StartedAt,
147
- DueAt: attempt.DueAt,
148
- FinishedAt: attempt.FinishedAt,
149
- Score: attempt.Score,
150
- Questions: questions,
151
- }, nil
152
  }
153
 
154
  func (s *quizService) calculateQuizScore(
155
  ctx context.Context,
 
156
  quiz *models.Quiz,
157
  attempt *models.QuizAttempt,
158
  ) error {
159
- // ambil total question dari quiz
160
- totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
161
  if err != nil {
162
  return err
163
  }
@@ -171,14 +202,14 @@ func (s *quizService) calculateQuizScore(
171
  return nil
172
  }
173
 
174
- // ambil semua user answer yang is_correct nya true
175
- correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
176
  if err != nil {
177
  return err
178
  }
179
 
 
180
  score := float64(correctAnswer) / float64(totalQuestion) * 100
181
-
182
  attempt.Score = score
183
  attempt.TotalQuestions = totalQuestion
184
  attempt.TotalCorrectAnswer = correctAnswer
@@ -192,14 +223,14 @@ func (s *quizService) calculateQuizScore(
192
  }
193
 
194
  func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
195
- // pastiin dulu attemp nya ada atau tidak
196
- _, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
197
  if err != nil {
198
  return nil, response.HandleGormError(err, "Internal Server Error")
199
  }
200
 
201
- // ambil pertanyaan dan jawaban pengguna
202
- question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
203
  if err != nil {
204
  if errors.Is(err, gorm.ErrRecordNotFound) {
205
  return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
@@ -211,106 +242,154 @@ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQu
211
  }
212
 
213
  func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
 
214
 
215
- correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, req.QuestionID)
216
- if err != nil {
217
- return nil, response.HandleGormError(err, "Internal Server Error")
218
- }
219
-
220
- question, err := s.quizRepository.UserGetUserAnswer(ctx, req.AttemptID, req.QuestionID)
221
- if err != nil {
222
 
223
- // jika belum ada answer
224
- if errors.Is(err, gorm.ErrRecordNotFound) {
225
- userAnswer := models.UserAnswer{
226
- QuizAttemptID: req.AttemptID,
227
- QuestionID: req.QuestionID,
228
- AnswerID: req.AnswerID,
229
- IsDoubt: req.IsDoubt,
230
- IsCorrect: false,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
 
233
- if req.AnswerID != nil {
234
- userAnswer.IsCorrect = *req.AnswerID == correctOptionID
235
- }
236
 
237
- if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
238
- return nil, response.HandleGormError(err, "Internal Server Error")
239
- }
 
 
 
240
 
241
- res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
242
- if err != nil {
243
- return nil, response.HandleGormError(err, "Internal Server Error")
244
- }
245
 
246
- return res, nil
 
 
 
 
 
247
  }
248
 
249
- return nil, response.HandleGormError(err, "Internal Server Error")
 
 
 
 
 
250
  }
251
 
252
- // updating answer
253
- question.AnswerID = req.AnswerID
254
- question.IsDoubt = req.IsDoubt
255
 
256
- if req.AnswerID != nil {
257
- question.IsCorrect = *req.AnswerID == correctOptionID
258
- }
259
 
260
- if err := s.quizRepository.UserSaveUserAnswer(ctx, question); err != nil {
261
- return nil, response.HandleGormError(err, "Internal Server Error")
262
- }
 
 
 
263
 
264
- res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
265
- if err != nil {
266
- if errors.Is(err, gorm.ErrRecordNotFound) {
267
- return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
268
  }
269
- return nil, response.HandleGormError(err, "Internal Server Error")
270
- }
271
 
272
- return res, nil
273
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
276
- attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
277
  if err != nil {
278
- return nil, response.HandleGormError(err, "Internal Server Error")
279
  }
280
 
281
- if err := s.calculateQuizScore(ctx, attempt.Quiz, attempt); err != nil {
282
- return nil, response.HandleGormError(err, "Internal Server Error")
283
- }
284
 
285
- if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
 
 
 
286
  return nil, response.HandleGormError(err, "Internal Server Error")
287
  }
288
 
289
- remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
 
290
  if err != nil {
291
  return nil, response.HandleGormError(err, "Internal Server Error")
292
  }
293
 
294
- if remainingAttempts == 0 {
295
- // hapus progress dan semua attempt pada quiz
296
- if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
297
- return nil, response.HandleGormError(err, "Internal Server Error")
298
- }
299
- }
300
-
301
- return &models.SubmitQuizResponse{
302
  QuizAttempt: *attempt,
303
  RemainingAttempts: remainingAttempts,
304
  }, nil
305
  }
306
 
307
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
308
- attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
 
309
  if err != nil {
310
  return nil, response.HandleGormError(err, "Internal Server Error")
311
  }
312
 
313
- review, err := s.quizRepository.UserGetReviewQuiz(ctx, attempt.ID, attempt.AccountID, attempt.QuizID)
 
314
  if err != nil {
315
  return nil, response.HandleGormError(err, "Internal Server Error")
316
  }
 
5
  "errors"
6
  "time"
7
 
8
+ "api.qobiltu.id/config"
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/validation"
11
  "api.qobiltu.id/repositories"
 
23
  UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
24
  UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
25
  UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
26
+ UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
27
  UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
28
  }
29
 
30
  type quizService struct {
31
+ db *gorm.DB
32
  quizRepository repositories.QuizRepository
33
  academyRepository repositories.AcademyRepository
34
  validator *validation.Validator
35
  }
36
 
37
+ func NewQuizService(db *gorm.DB, quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
38
+ return &quizService{
39
+ db: db,
40
+ quizRepository: quizRepository,
41
+ academyRepository: academyRepository,
42
+ validator: validator,
43
+ }
44
  }
45
 
46
  func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
47
+ quiz, err := s.quizRepository.UserGetQuiz(ctx, s.db, req)
48
  if err != nil {
49
  return nil, response.HandleGormError(err, "Internal Server Error")
50
  }
 
53
  }
54
 
55
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
56
+ // Fetch quiz data
57
+ quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, s.db, &models.UserGetQuizRequest{
58
  AccountID: req.AccountID,
59
  AcademyID: req.AcademyID,
60
  })
 
64
 
65
  quiz := &quizAttempt.Quiz
66
 
67
+ // check is already passed
68
+ isPassedQuiz, err := s.quizRepository.UserIsPassedQuiz(ctx, s.db, req.AccountID, quiz.ID)
69
+ if err != nil {
70
+ return nil, response.HandleGormError(err, "Internal Server Error")
71
+ }
72
+
73
+ if isPassedQuiz {
74
+ return nil, models.Exception{QuizAlreadyPassed: true, Message: "You have already passed this quiz"}
75
+ }
76
+
77
+ // Check for active attempt
78
+ existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, s.db, req.AccountID, quiz.ID)
79
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
80
  return nil, response.HandleGormError(err, "Internal Server Error")
81
  }
82
 
83
+ // Handle existing active attempt
84
  if existingAttempt != nil {
85
  return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
86
  }
87
 
88
+ // Handle new attempt
89
  return s.handleNewAttempt(ctx, req, quiz)
90
  }
91
 
92
  func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
93
  now := time.Now()
94
 
95
+ // Check if attempt has expired
96
  if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
 
97
  return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
98
  }
99
 
100
+ // Fetch questions for active attempt
101
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, s.db, req.AccountID, quiz.ID, attempt.ID)
102
  if err != nil {
103
  return nil, response.HandleGormError(err, "Internal Server Error")
104
  }
 
118
  }
119
 
120
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
121
+ var result *models.UserAttemptQuizResponse
122
 
123
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
124
+ // Check total attempts
125
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, tx, req.AccountID, quiz.ID)
126
+ if err != nil {
127
+ return response.HandleGormError(err, "Internal Server Error")
128
+ }
129
+ if totalAttempts >= quiz.AttemptLimit {
130
+ return models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
131
+ }
132
 
133
+ // Check academy progress
134
+ percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, tx, req.AccountID, req.AcademyID)
135
+ if err != nil {
136
+ return response.HandleGormError(err, "Internal Server Error")
137
+ }
138
+ if percentage < 100 {
139
+ return models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
140
+ }
141
 
142
+ // Create new attempt
143
+ attempt := models.QuizAttempt{
144
+ AccountID: req.AccountID,
145
+ QuizID: quiz.ID,
146
+ AcademyID: quiz.AcademyID,
147
+ StartedAt: time.Now(),
148
+ DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
149
+ }
150
 
151
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, tx, &attempt); err != nil {
152
+ return response.HandleGormError(err, "Internal Server Error")
153
+ }
154
+
155
+ // Fetch questions
156
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, tx, req.AccountID, quiz.ID, attempt.ID)
157
+ if err != nil {
158
+ return response.HandleGormError(err, "Internal Server Error")
159
+ }
160
+
161
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
162
+
163
+ result = &models.UserAttemptQuizResponse{
164
+ ID: attempt.ID,
165
+ AccountID: attempt.AccountID,
166
+ QuizID: attempt.QuizID,
167
+ StartedAt: attempt.StartedAt,
168
+ DueAt: attempt.DueAt,
169
+ FinishedAt: attempt.FinishedAt,
170
+ Score: attempt.Score,
171
+ Questions: questions,
172
+ }
173
+
174
+ return nil
175
+ })
176
 
 
 
177
  if err != nil {
178
+ return nil, models.Exception{InternalServerError: true, Message: "Internal Server Error"}
179
  }
180
 
181
+ return result, nil
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
  func (s *quizService) calculateQuizScore(
185
  ctx context.Context,
186
+ tx *gorm.DB,
187
  quiz *models.Quiz,
188
  attempt *models.QuizAttempt,
189
  ) error {
190
+ // Fetch total questions
191
+ totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, tx, attempt.QuizID)
192
  if err != nil {
193
  return err
194
  }
 
202
  return nil
203
  }
204
 
205
+ // Fetch total correct answers
206
+ correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, tx, attempt.ID)
207
  if err != nil {
208
  return err
209
  }
210
 
211
+ // Calculate score
212
  score := float64(correctAnswer) / float64(totalQuestion) * 100
 
213
  attempt.Score = score
214
  attempt.TotalQuestions = totalQuestion
215
  attempt.TotalCorrectAnswer = correctAnswer
 
223
  }
224
 
225
  func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
226
+ // Check if attempt exists
227
+ _, err := s.quizRepository.UserGetAttemptByID(ctx, s.db, req.AttemptID)
228
  if err != nil {
229
  return nil, response.HandleGormError(err, "Internal Server Error")
230
  }
231
 
232
+ // Fetch question and user answer
233
+ question, err := s.quizRepository.UserGetQuestionQuiz(ctx, s.db, req.AttemptID, req.QuestionID)
234
  if err != nil {
235
  if errors.Is(err, gorm.ErrRecordNotFound) {
236
  return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
 
242
  }
243
 
244
  func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
245
+ var result *models.GetQuestionQuizResponse
246
 
247
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
248
+ // Fetch correct option ID
249
+ correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, tx, req.QuestionID)
250
+ if err != nil {
251
+ return response.HandleGormError(err, "Internal Server Error")
252
+ }
 
253
 
254
+ // Check if user answer exists
255
+ question, err := s.quizRepository.UserGetUserAnswer(ctx, tx, req.AttemptID, req.QuestionID)
256
+ if err != nil {
257
+ // If no answer exists, create a new one
258
+ if errors.Is(err, gorm.ErrRecordNotFound) {
259
+ userAnswer := models.UserAnswer{
260
+ QuizAttemptID: req.AttemptID,
261
+ QuestionID: req.QuestionID,
262
+ AnswerID: req.AnswerID,
263
+ IsDoubt: req.IsDoubt,
264
+ IsCorrect: false,
265
+ }
266
+
267
+ if req.AnswerID != nil {
268
+ userAnswer.IsCorrect = *req.AnswerID == correctOptionID
269
+ }
270
+
271
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, &userAnswer); err != nil {
272
+ return response.HandleGormError(err, "Internal Server Error")
273
+ }
274
+
275
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
276
+ if err != nil {
277
+ return response.HandleGormError(err, "Internal Server Error")
278
+ }
279
+
280
+ result = res
281
+ return nil
282
  }
283
 
284
+ return response.HandleGormError(err, "Internal Server Error")
285
+ }
 
286
 
287
+ // Update existing answer
288
+ question.AnswerID = req.AnswerID
289
+ question.IsDoubt = req.IsDoubt
290
+ if req.AnswerID != nil {
291
+ question.IsCorrect = *req.AnswerID == correctOptionID
292
+ }
293
 
294
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, question); err != nil {
295
+ return response.HandleGormError(err, "Internal Server Error")
296
+ }
 
297
 
298
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
299
+ if err != nil {
300
+ if errors.Is(err, gorm.ErrRecordNotFound) {
301
+ return models.Exception{DataNotFound: true, Message: "Question not found"}
302
+ }
303
+ return response.HandleGormError(err, "Internal Server Error")
304
  }
305
 
306
+ result = res
307
+ return nil
308
+ })
309
+
310
+ if err != nil {
311
+ return nil, err
312
  }
313
 
314
+ return result, nil
315
+ }
 
316
 
317
+ func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
318
+ var result *models.SubmitQuizResponse
 
319
 
320
+ err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
321
+ // Fetch attempt
322
+ attempt, err := s.quizRepository.UserGetAttemptByID(ctx, tx, req.AttemptID)
323
+ if err != nil {
324
+ return response.HandleGormError(err, "Internal Server Error")
325
+ }
326
 
327
+ // Calculate score
328
+ if err := s.calculateQuizScore(ctx, tx, attempt.Quiz, attempt); err != nil {
329
+ return response.HandleGormError(err, "Internal Server Error")
 
330
  }
 
 
331
 
332
+ // Update attempt
333
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, tx, attempt); err != nil {
334
+ return response.HandleGormError(err, "Internal Server Error")
335
+ }
336
+
337
+ // Get remaining attempts
338
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, tx, attempt.AccountID, attempt.AcademyID)
339
+ if err != nil {
340
+ return response.HandleGormError(err, "Internal Server Error")
341
+ }
342
+
343
+ // If no remaining attempts, delete progress and attempts
344
+ if remainingAttempts == 0 {
345
+ if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, tx, attempt.AccountID, attempt.AcademyID); err != nil {
346
+ return response.HandleGormError(err, "Internal Server Error")
347
+ }
348
+ }
349
+
350
+ result = &models.SubmitQuizResponse{
351
+ QuizAttempt: *attempt,
352
+ RemainingAttempts: remainingAttempts,
353
+ }
354
+
355
+ return nil
356
+ })
357
 
 
 
358
  if err != nil {
359
+ return nil, err
360
  }
361
 
362
+ return result, nil
363
+ }
 
364
 
365
+ func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
366
+ // Fetch last attempt
367
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
368
+ if err != nil {
369
  return nil, response.HandleGormError(err, "Internal Server Error")
370
  }
371
 
372
+ // Get remaining attempts
373
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, s.db, attempt.AccountID, attempt.AcademyID)
374
  if err != nil {
375
  return nil, response.HandleGormError(err, "Internal Server Error")
376
  }
377
 
378
+ return &models.ResultQuizResponse{
 
 
 
 
 
 
 
379
  QuizAttempt: *attempt,
380
  RemainingAttempts: remainingAttempts,
381
  }, nil
382
  }
383
 
384
  func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
385
+ // Fetch last attempt
386
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
387
  if err != nil {
388
  return nil, response.HandleGormError(err, "Internal Server Error")
389
  }
390
 
391
+ // Fetch review
392
+ review, err := s.quizRepository.UserGetReviewQuiz(ctx, s.db, attempt.ID, attempt.AccountID, attempt.QuizID)
393
  if err != nil {
394
  return nil, response.HandleGormError(err, "Internal Server Error")
395
  }
space/space/space/repositories/quiz_repository.go CHANGED
@@ -65,6 +65,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
65
  quiz_attempts qa
66
  ON q.id = qa.quiz_id
67
  AND qa.account_id = @accountID
 
68
  WHERE
69
  q.academy_id = @academyID
70
  GROUP BY
@@ -77,7 +78,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
77
  "academyID": req.AcademyID,
78
  }).Scan(&quizResponse).Error
79
  if err != nil {
80
- return nil, err
81
  }
82
 
83
  if quizResponse.ID == 0 {
 
65
  quiz_attempts qa
66
  ON q.id = qa.quiz_id
67
  AND qa.account_id = @accountID
68
+ AND qa.finished_at IS NOT NULL
69
  WHERE
70
  q.academy_id = @academyID
71
  GROUP BY
 
78
  "academyID": req.AcademyID,
79
  }).Scan(&quizResponse).Error
80
  if err != nil {
81
+ return nil, fmt.Errorf("failed to query quiz: %w", err)
82
  }
83
 
84
  if quizResponse.ID == 0 {
space/space/space/space/models/field_counter.go CHANGED
@@ -164,7 +164,7 @@ func isFieldFilled(field reflect.Value) bool {
164
  case reflect.Float32, reflect.Float64:
165
  return field.Float() != 0
166
  case reflect.Bool:
167
- return field.Bool()
168
  case reflect.Slice, reflect.Map, reflect.Array:
169
  return !field.IsNil() && field.Len() > 0
170
  case reflect.Struct:
 
164
  case reflect.Float32, reflect.Float64:
165
  return field.Float() != 0
166
  case reflect.Bool:
167
+ return true
168
  case reflect.Slice, reflect.Map, reflect.Array:
169
  return !field.IsNil() && field.Len() > 0
170
  case reflect.Struct:
space/space/space/space/pkg/validation/validation.go CHANGED
@@ -21,9 +21,9 @@ func New(db *gorm.DB) (*Validator, error) {
21
  return nil, err
22
  }
23
 
24
- // Start background refresh every 10 minutes
25
  ctx := context.Background()
26
- dbSource.StartAutoRefresh(ctx, 10*time.Minute)
27
 
28
  // create validator
29
  validator := NewValidator(dbSource)
 
21
  return nil, err
22
  }
23
 
24
+ // Start background refresh every 5 minutes
25
  ctx := context.Background()
26
+ dbSource.StartAutoRefresh(ctx, 5*time.Minute)
27
 
28
  // create validator
29
  validator := NewValidator(dbSource)
space/space/space/space/services/cv_service.go CHANGED
@@ -192,6 +192,9 @@ func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *model
192
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
193
  res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
194
  if err != nil {
 
 
 
195
  return nil, response.HandleGormError(err, "Internal Server Error")
196
  }
197
 
@@ -312,7 +315,9 @@ func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.SaveP
312
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
313
  res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
314
  if err != nil {
315
- return nil, response.HandleGormError(err, "Internal Server Error")
 
 
316
  }
317
  return res, nil
318
  }
@@ -369,7 +374,7 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
369
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
370
  res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
371
  if err != nil {
372
- return nil, response.HandleGormError(err, "Internal Server Error")
373
  }
374
  return res, nil
375
  }
 
192
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
193
  res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
194
  if err != nil {
195
+ if errors.Is(err, gorm.ErrRecordNotFound) {
196
+ return &models.PersonalityAndPreferenceCV{}, nil
197
+ }
198
  return nil, response.HandleGormError(err, "Internal Server Error")
199
  }
200
 
 
315
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
316
  res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
317
  if err != nil {
318
+ if errors.Is(err, gorm.ErrRecordNotFound) {
319
+ return &models.PhysicalAndHealthCV{}, nil
320
+ }
321
  }
322
  return res, nil
323
  }
 
374
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
375
  res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
376
  if err != nil {
377
+ return &models.WorshipAndReligiousUnderstandingCV{}, nil
378
  }
379
  return res, nil
380
  }
space/space/space/space/space/controller/quiz/quiz_controller.go CHANGED
@@ -17,6 +17,10 @@ type QuizController interface {
17
  // === USER ===
18
  UserGetQuiz(ctx *gin.Context)
19
  UserAttemptQuiz(ctx *gin.Context)
 
 
 
 
20
  }
21
 
22
  type quizController struct {
@@ -30,7 +34,7 @@ func NewQuizController(quizService services.QuizService) QuizController {
30
  }
31
 
32
  func (c *quizController) UserGetQuiz(ctx *gin.Context) {
33
- academyID := ctx.Param("id")
34
  academyIDInt, err := strconv.Atoi(academyID)
35
  if err != nil {
36
  response.HandleError(ctx, err)
@@ -38,6 +42,7 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
38
  }
39
 
40
  accountData := middleware.GetAccountData(ctx)
 
41
  req := models.UserGetQuizRequest{
42
  AccountID: int64(accountData.UserID),
43
  AcademyID: int64(academyIDInt),
@@ -49,11 +54,11 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
49
  return
50
  }
51
 
52
- response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", http.StatusOK, res)
53
  }
54
 
55
  func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
56
- academyID := ctx.Param("id")
57
  academyIDInt, err := strconv.Atoi(academyID)
58
  if err != nil {
59
  response.HandleError(ctx, err)
@@ -61,6 +66,7 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
61
  }
62
 
63
  accountData := middleware.GetAccountData(ctx)
 
64
  req := models.UserAttemptQuizRequest{
65
  AccountID: int64(accountData.UserID),
66
  AcademyID: int64(academyIDInt),
@@ -72,5 +78,137 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
72
  return
73
  }
74
 
75
- response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", http.StatusOK, res)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
 
17
  // === USER ===
18
  UserGetQuiz(ctx *gin.Context)
19
  UserAttemptQuiz(ctx *gin.Context)
20
+ UserGetQuestionQuiz(ctx *gin.Context)
21
+ UserAnswerQuiz(ctx *gin.Context)
22
+ UserSubmitQuiz(ctx *gin.Context)
23
+ UserReviewQuiz(ctx *gin.Context)
24
  }
25
 
26
  type quizController struct {
 
34
  }
35
 
36
  func (c *quizController) UserGetQuiz(ctx *gin.Context) {
37
+ academyID := ctx.Query("academy_id")
38
  academyIDInt, err := strconv.Atoi(academyID)
39
  if err != nil {
40
  response.HandleError(ctx, err)
 
42
  }
43
 
44
  accountData := middleware.GetAccountData(ctx)
45
+
46
  req := models.UserGetQuizRequest{
47
  AccountID: int64(accountData.UserID),
48
  AcademyID: int64(academyIDInt),
 
54
  return
55
  }
56
 
57
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", res, nil)
58
  }
59
 
60
  func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
61
+ academyID := ctx.Query("academy_id")
62
  academyIDInt, err := strconv.Atoi(academyID)
63
  if err != nil {
64
  response.HandleError(ctx, err)
 
66
  }
67
 
68
  accountData := middleware.GetAccountData(ctx)
69
+
70
  req := models.UserAttemptQuizRequest{
71
  AccountID: int64(accountData.UserID),
72
  AcademyID: int64(academyIDInt),
 
78
  return
79
  }
80
 
81
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", res, nil)
82
+ }
83
+
84
+ func (c *quizController) UserGetQuestionQuiz(ctx *gin.Context) {
85
+ academyID := ctx.Query("academy_id")
86
+ academyIDInt, err := strconv.Atoi(academyID)
87
+ if err != nil {
88
+ response.HandleError(ctx, err)
89
+ return
90
+ }
91
+
92
+ questionID := ctx.Query("question_id")
93
+ questionIDInt, err := strconv.Atoi(questionID)
94
+ if err != nil {
95
+ response.HandleError(ctx, err)
96
+ return
97
+ }
98
+
99
+ attemptID := ctx.Query("attempt_id")
100
+ attemptIDInt, err := strconv.Atoi(attemptID)
101
+ if err != nil {
102
+ response.HandleError(ctx, err)
103
+ return
104
+ }
105
+
106
+ accountData := middleware.GetAccountData(ctx)
107
+
108
+ req := models.GetQuestionQuizRequest{
109
+ AccountID: int64(accountData.UserID),
110
+ AcademyID: int64(academyIDInt),
111
+ QuestionID: int64(questionIDInt),
112
+ AttemptID: int64(attemptIDInt),
113
+ }
114
+
115
+ res, err := c.quizService.UserGetQuestionQuiz(ctx, &req)
116
+ if err != nil {
117
+ response.HandleError(ctx, err)
118
+ return
119
+ }
120
+
121
+ response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
122
+ }
123
+
124
+ func (c *quizController) UserAnswerQuiz(ctx *gin.Context) {
125
+ var req models.AnswerQuizRequest
126
+ if err := ctx.ShouldBindJSON(&req); err != nil {
127
+ response.HandleError(ctx, err)
128
+ return
129
+ }
130
+
131
+ academyID := ctx.Query("academy_id")
132
+ academyIDInt, err := strconv.Atoi(academyID)
133
+ if err != nil {
134
+ response.HandleError(ctx, err)
135
+ return
136
+ }
137
+
138
+ attemptID := ctx.Query("attempt_id")
139
+ attemptIDInt, err := strconv.Atoi(attemptID)
140
+ if err != nil {
141
+ response.HandleError(ctx, err)
142
+ return
143
+ }
144
+
145
+ accountData := middleware.GetAccountData(ctx)
146
+
147
+ req.AccountID = int64(accountData.UserID)
148
+ req.AcademyID = int64(academyIDInt)
149
+ req.AttemptID = int64(attemptIDInt)
150
+
151
+ res, err := c.quizService.UserAnswerQuiz(ctx, &req)
152
+ if err != nil {
153
+ response.HandleError(ctx, err)
154
+ return
155
+ }
156
+
157
+ response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
158
+ }
159
+
160
+ func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
161
+ academyID := ctx.Query("academy_id")
162
+ academyIDInt, err := strconv.Atoi(academyID)
163
+ if err != nil {
164
+ response.HandleError(ctx, err)
165
+ return
166
+ }
167
+
168
+ attemptID := ctx.Query("attempt_id")
169
+ attemptIDInt, err := strconv.Atoi(attemptID)
170
+ if err != nil {
171
+ response.HandleError(ctx, err)
172
+ return
173
+ }
174
+
175
+ accountData := middleware.GetAccountData(ctx)
176
+
177
+ req := models.SubmitQuizRequest{
178
+ AccountID: int64(accountData.UserID),
179
+ AcademyID: int64(academyIDInt),
180
+ AttemptID: int64(attemptIDInt),
181
+ }
182
+
183
+ res, err := c.quizService.UserSubmitQuiz(ctx, &req)
184
+ if err != nil {
185
+ response.HandleError(ctx, err)
186
+ return
187
+ }
188
+
189
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
190
+ }
191
+
192
+ func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
193
+ academyID := ctx.Query("academy_id")
194
+ academyIDInt, err := strconv.Atoi(academyID)
195
+ if err != nil {
196
+ response.HandleError(ctx, err)
197
+ return
198
+ }
199
+
200
+ accountData := middleware.GetAccountData(ctx)
201
+
202
+ req := models.ReviewQuizRequest{
203
+ AccountID: int64(accountData.UserID),
204
+ AcademyID: int64(academyIDInt),
205
+ }
206
+
207
+ res, err := c.quizService.UserReviewQuiz(ctx, &req)
208
+ if err != nil {
209
+ response.HandleError(ctx, err)
210
+ return
211
+ }
212
+
213
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz review retrieved successfully", res.Reviews, nil)
214
  }
space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -156,7 +156,7 @@ type (
156
  Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
  Content string `gorm:"column:content" json:"content"`
158
  Order int `gorm:"column:order" json:"order"`
159
- Review string `gorm:"column:reviews" json:"reviews"`
160
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
  }
@@ -172,17 +172,23 @@ type (
172
  }
173
 
174
  QuizAttempt struct {
175
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
- AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
- QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
179
- Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
180
- StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
181
- DueAt time.Time `gorm:"column:due_at" json:"due_at"`
182
- FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
183
- Score float64 `gorm:"column:score" json:"score"`
184
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
185
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
 
 
 
 
 
186
  }
187
 
188
  UserAnswer struct {
@@ -193,7 +199,7 @@ type (
193
  Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
194
  AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
195
  Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
196
- IsDoubt bool `gorm:"column:id_doubt" json:"id_doubt"`
197
  IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
198
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
199
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
156
  Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
  Content string `gorm:"column:content" json:"content"`
158
  Order int `gorm:"column:order" json:"order"`
159
+ Review string `gorm:"column:review" json:"review"`
160
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
  }
 
172
  }
173
 
174
  QuizAttempt struct {
175
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
+ AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
179
+ Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
180
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
181
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"-"`
182
+ StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
183
+ DueAt time.Time `gorm:"column:due_at" json:"due_at"`
184
+ FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
185
+ TotalQuestions int64 `gorm:"column:total_questions;not null" json:"total_questions"`
186
+ TotalCorrectAnswer int64 `gorm:"column:total_correct_answer;not null" json:"total_correct_answer"`
187
+ TotalWrongAnswer int64 `gorm:"column:total_wrong_answer;not null" json:"total_wrong_answer"`
188
+ Score float64 `gorm:"column:score" json:"score"`
189
+ IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
190
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
191
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
192
  }
193
 
194
  UserAnswer struct {
 
199
  Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
200
  AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
201
  Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
202
+ IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
203
  IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
204
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
205
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
space/space/space/space/space/models/request_model.go CHANGED
@@ -152,6 +152,7 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
152
  }
153
 
154
  type (
 
155
  UserGetQuizRequest struct {
156
  AccountID int64 `json:"account_id" validate:"required"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
@@ -159,36 +160,105 @@ type (
159
 
160
  UserGetQuizResponse struct {
161
  Quiz
162
- TotalQuestions int64 `json:"total_questions"`
163
- UserAttempts int64 `json:"user_attempts"`
 
164
  }
165
 
 
166
  UserAttemptQuizRequest struct {
167
- AccountID int64 `json:"account_id"`
168
- AcademyID int64 `json:"academy_id"`
169
  }
170
 
171
  UserAttemptQuizQuestionsResponse struct {
172
- ID int64 `gorm:"column:id" json:"id"`
173
- IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
174
- IsAnswered bool `gorm:"column:is_answered" json:"is_answered"`
175
  }
176
 
177
  UserAttemptQuizResponse struct {
178
- QuizAttempt
179
- Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
 
 
 
 
 
 
180
  }
181
 
 
182
  GetQuestionQuizRequest struct {
183
- AccountID int64 `json:"account_id"`
184
- QuizID int64 `json:"quiz_id" validate:"required"`
 
 
185
  }
186
 
187
  GetQuestionQuizResponse struct {
188
- Question Question `json:"question"`
189
- Answer []Answer `json:"answer_options"`
190
- UserAnswer int `json:"current_user_answer"`
191
- IsDoubt bool `json:"is_doubt"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  )
194
 
@@ -239,11 +309,6 @@ type QuestionQuizRequest struct {
239
  QuestionNo int `json:"question_no" binding:"required"`
240
  }
241
 
242
- type AnswerQuizRequest struct {
243
- QuestionNo int `json:"question_no" binding:"required"`
244
- Answer int `json:"answer" binding:"required"`
245
- }
246
-
247
  type (
248
  ListCitiesByProvinceIdRequest struct {
249
  ProvinceID int64 `json:"province_id" binding:"required"`
 
152
  }
153
 
154
  type (
155
+ // GET QUIZ
156
  UserGetQuizRequest struct {
157
  AccountID int64 `json:"account_id" validate:"required"`
158
  AcademyID int64 `json:"academy_id" validate:"required"`
 
160
 
161
  UserGetQuizResponse struct {
162
  Quiz
163
+ TotalQuestions int64 `json:"total_questions"`
164
+ UserAttempts int64 `json:"user_attempts"`
165
+ HasActiveAttempt bool `json:"has_active_attempt"`
166
  }
167
 
168
+ // ATTEMP QUIZ
169
  UserAttemptQuizRequest struct {
170
+ AccountID int64 `json:"account_id" validate:"required"`
171
+ AcademyID int64 `json:"academy_id" validate:"required"`
172
  }
173
 
174
  UserAttemptQuizQuestionsResponse struct {
175
+ ID int64 `json:"id"`
176
+ IsDoubt bool `json:"is_doubt"`
177
+ IsAnswered bool `json:"is_answered"`
178
  }
179
 
180
  UserAttemptQuizResponse struct {
181
+ ID int64 `json:"id"`
182
+ AccountID int64 `json:"account_id"`
183
+ QuizID int64 `json:"quiz_id"`
184
+ StartedAt time.Time `json:"started_at"`
185
+ DueAt time.Time `json:"due_at"`
186
+ FinishedAt *time.Time `json:"finished_at"`
187
+ Score float64 `json:"score"`
188
+ Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
189
  }
190
 
191
+ // GET QUESTION
192
  GetQuestionQuizRequest struct {
193
+ AccountID int64 `json:"account_id" validate:"required"`
194
+ AcademyID int64 `json:"academy_id" validate:"required"`
195
+ AttemptID int64 `json:"attempt_id" validate:"required"`
196
+ QuestionID int64 `json:"question_id" validate:"required"`
197
  }
198
 
199
  GetQuestionQuizResponse struct {
200
+ Question struct {
201
+ ID int64 `json:"id"`
202
+ QuizID int64 `json:"quiz_id"`
203
+ Content string `json:"content"`
204
+ } `json:"question"`
205
+ AnswerOptions []struct {
206
+ ID int64 `json:"id"`
207
+ Content string `json:"content"`
208
+ } `json:"answer_options"`
209
+ AnswerID *int64 `json:"answer_id"`
210
+ IsDoubt bool `json:"is_doubt"`
211
+ IsAnswered bool `json:"is_answered"`
212
+ }
213
+
214
+ // ANSWER QUESTION
215
+ AnswerQuizRequest struct {
216
+ AccountID int64 `json:"account_id" validate:"required"`
217
+ AcademyID int64 `json:"academy_id" validate:"required"`
218
+ AttemptID int64 `json:"attempt_id" validate:"required"`
219
+
220
+ QuestionID int64 `json:"question_id" validate:"required"`
221
+ AnswerID *int64 `json:"answer_id"`
222
+ IsDoubt bool `json:"is_doubt"`
223
+ }
224
+
225
+ // SUBMIT QUIZ
226
+ SubmitQuizRequest struct {
227
+ AccountID int64 `json:"account_id" validate:"required"`
228
+ AcademyID int64 `json:"academy_id" validate:"required"`
229
+ AttemptID int64 `json:"attempt_id" validate:"required"`
230
+ }
231
+
232
+ SubmitQuizResponse struct {
233
+ QuizAttempt
234
+ RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
235
+ }
236
+
237
+ // REVIEW QUIZ
238
+ ReviewQuizRequest struct {
239
+ AccountID int64 `json:"account_id" validate:"required"`
240
+ AcademyID int64 `json:"academy_id" validate:"required"`
241
+ }
242
+
243
+ ReviewQuizQuestion struct {
244
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
245
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
246
+ Content string `gorm:"column:content" json:"content"`
247
+ Review string `gorm:"column:review" json:"review"`
248
+ }
249
+
250
+ ReviewQuiz struct {
251
+ Question ReviewQuizQuestion `json:"question"`
252
+ AnswerOptions []struct {
253
+ ID int64 `json:"id"`
254
+ Content string `json:"content"`
255
+ IsCorrect bool `json:"is_correct"`
256
+ } `json:"answer_options"`
257
+ AnswerID int64 `json:"answer_id"`
258
+ }
259
+
260
+ ReviewQuizResponse struct {
261
+ Reviews []ReviewQuiz
262
  }
263
  )
264
 
 
309
  QuestionNo int `json:"question_no" binding:"required"`
310
  }
311
 
 
 
 
 
 
312
  type (
313
  ListCitiesByProvinceIdRequest struct {
314
  ProvinceID int64 `json:"province_id" binding:"required"`
space/space/space/space/space/repositories/quiz_repository.go CHANGED
@@ -2,6 +2,8 @@ package repositories
2
 
3
  import (
4
  "context"
 
 
5
 
6
  "api.qobiltu.id/models"
7
  "gorm.io/gorm"
@@ -10,13 +12,25 @@ import (
10
  type QuizRepository interface {
11
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
12
  UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
13
- UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
14
- UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
15
- UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
16
  UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
 
 
 
 
 
 
 
 
 
17
  UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
18
  UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
19
  UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
 
 
 
 
 
 
20
  }
21
 
22
  type quizRepository struct {
@@ -31,24 +45,37 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
31
  var quizResponse models.UserGetQuizResponse
32
 
33
  rawQuery := `
34
- SELECT
35
- q.*,
36
- (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
37
- COALESCE(COUNT(qa.id), 0) AS user_attempts
38
- FROM
39
- quizzes q
40
- LEFT JOIN
41
- quiz_attempts qa
42
- ON q.id = qa.quiz_id
43
- AND qa.account_id = ?
44
- WHERE
45
- q.academy_id = ?
46
- GROUP BY
47
- q.id, q.academy_id, q.slug, q.title, q.description,
48
- q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
49
- `
 
 
 
 
 
 
 
 
 
 
50
 
51
- err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
 
 
 
52
  if err != nil {
53
  return nil, err
54
  }
@@ -63,7 +90,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
63
  func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
64
  var quizAttempt models.QuizAttempt
65
 
66
- err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
67
  if err != nil {
68
  return nil, err
69
  }
@@ -71,8 +98,19 @@ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID
71
  return &quizAttempt, nil
72
  }
73
 
74
- func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
75
- return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
  func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
@@ -105,7 +143,7 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
105
  q.id;
106
  `
107
 
108
- err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
109
  if err != nil {
110
  return nil, err
111
  }
@@ -113,19 +151,21 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
113
  return questions, nil
114
  }
115
 
116
- func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
117
- return r.db.Create(attempt).Error
118
- }
119
-
120
- func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
121
- var totalAttempts int64
122
 
123
- err := r.db.Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
 
 
124
  if err != nil {
125
- return 0, err
126
  }
127
 
128
- return totalAttempts, nil
 
 
 
 
129
  }
130
 
131
  func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
@@ -153,3 +193,205 @@ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quiz
153
  func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
154
  return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
155
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import (
4
  "context"
5
+ "encoding/json"
6
+ "fmt"
7
 
8
  "api.qobiltu.id/models"
9
  "gorm.io/gorm"
 
12
  type QuizRepository interface {
13
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
14
  UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
 
 
 
15
  UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
16
+ UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
17
+ UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
18
+
19
+ UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error)
20
+ UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
21
+
22
+ UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
23
+ UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error)
24
+
25
  UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
26
  UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
27
  UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
28
+ UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error)
29
+ UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
30
+ UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error)
31
+ UserSaveUserAnswer(ctx context.Context, answer *models.UserAnswer) error
32
+ GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error)
33
+ UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error
34
  }
35
 
36
  type quizRepository struct {
 
45
  var quizResponse models.UserGetQuizResponse
46
 
47
  rawQuery := `
48
+ SELECT
49
+ q.*,
50
+ (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
51
+ COALESCE(COUNT(qa.id), 0) AS user_attempts,
52
+ CASE
53
+ WHEN EXISTS (
54
+ SELECT 1
55
+ FROM quiz_attempts qa2
56
+ WHERE qa2.quiz_id = q.id
57
+ AND qa2.account_id = @accountID
58
+ AND qa2.finished_at IS NULL
59
+ ) THEN TRUE
60
+ ELSE FALSE
61
+ END AS has_active_attempt
62
+ FROM
63
+ quizzes q
64
+ LEFT JOIN
65
+ quiz_attempts qa
66
+ ON q.id = qa.quiz_id
67
+ AND qa.account_id = @accountID
68
+ WHERE
69
+ q.academy_id = @academyID
70
+ GROUP BY
71
+ q.id, q.academy_id, q.slug, q.title, q.description,
72
+ q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
73
+ `
74
 
75
+ err := r.db.Debug().Raw(rawQuery, map[string]any{
76
+ "accountID": req.AccountID,
77
+ "academyID": req.AcademyID,
78
+ }).Scan(&quizResponse).Error
79
  if err != nil {
80
  return nil, err
81
  }
 
90
  func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
91
  var quizAttempt models.QuizAttempt
92
 
93
+ err := r.db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
94
  if err != nil {
95
  return nil, err
96
  }
 
98
  return &quizAttempt, nil
99
  }
100
 
101
+ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
102
+ var totalAttempts int64
103
+
104
+ err := r.db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
105
+ if err != nil {
106
+ return 0, err
107
+ }
108
+
109
+ return totalAttempts, nil
110
+ }
111
+
112
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
113
+ return r.db.Debug().Create(attempt).Error
114
  }
115
 
116
  func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
 
143
  q.id;
144
  `
145
 
146
+ err := r.db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
147
  if err != nil {
148
  return nil, err
149
  }
 
151
  return questions, nil
152
  }
153
 
154
+ func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error) {
155
+ var quizAttempt models.QuizAttempt
 
 
 
 
156
 
157
+ err := r.db.Where("id = ?", attemptID).
158
+ Preload("Quiz").
159
+ First(&quizAttempt).Error
160
  if err != nil {
161
+ return nil, err
162
  }
163
 
164
+ return &quizAttempt, nil
165
+ }
166
+
167
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
168
+ return r.db.Save(attempt).Error
169
  }
170
 
171
  func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
 
193
  func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
194
  return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
195
  }
196
+
197
+ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error) {
198
+ var remainingAttempts int64
199
+
200
+ rawQuery := `
201
+ SELECT
202
+ GREATEST(q.attempt_limit - COALESCE(COUNT(qa.id), 0), 0) AS remaining_attempts
203
+ FROM
204
+ quizzes q
205
+ LEFT JOIN
206
+ quiz_attempts qa
207
+ ON q.id = qa.quiz_id
208
+ AND qa.account_id = ?
209
+ AND qa.finished_at IS NOT NULL
210
+ WHERE
211
+ q.id = ?
212
+ GROUP BY
213
+ q.attempt_limit;
214
+ `
215
+
216
+ err := r.db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
217
+ if err != nil {
218
+ return 0, err
219
+ }
220
+
221
+ return remainingAttempts, nil
222
+ }
223
+
224
+ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
225
+ var resultJSON string
226
+ var response models.GetQuestionQuizResponse
227
+
228
+ rawQuery := `
229
+ SELECT
230
+ json_build_object(
231
+ 'question', json_build_object(
232
+ 'id', q.id,
233
+ 'quiz_id', q.quiz_id,
234
+ 'content', q.content
235
+ ),
236
+ 'answer_options', json_agg(
237
+ json_build_object(
238
+ 'id', a.id,
239
+ 'content', a.content
240
+ )
241
+ ),
242
+ 'answer_id', ua.selected_answer_id,
243
+ 'is_doubt', COALESCE(ua.is_doubt, FALSE),
244
+ 'is_answered', ua.selected_answer_id IS NOT NULL
245
+ ) AS result
246
+ FROM
247
+ questions q
248
+ LEFT JOIN
249
+ answers a ON q.id = a.question_id
250
+ LEFT JOIN
251
+ user_answers ua
252
+ ON q.id = ua.question_id
253
+ AND ua.quiz_attempt_id = $1
254
+ WHERE
255
+ q.id = $2
256
+ GROUP BY
257
+ q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
258
+ `
259
+
260
+ err := r.db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
261
+ if err != nil {
262
+ return nil, err
263
+ }
264
+
265
+ err = json.Unmarshal([]byte(resultJSON), &response)
266
+ if err != nil {
267
+ return nil, err
268
+ }
269
+
270
+ return &response, nil
271
+ }
272
+
273
+ func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int64) (*models.Question, error) {
274
+ var question models.Question
275
+
276
+ err := r.db.Where("id = ?", questionID).First(&question).Error
277
+ if err != nil {
278
+ return nil, err
279
+ }
280
+
281
+ return &question, nil
282
+ }
283
+
284
+ func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error) {
285
+ var userAnswer models.UserAnswer
286
+
287
+ err := r.db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
288
+ if err != nil {
289
+ return nil, err
290
+ }
291
+
292
+ return &userAnswer, nil
293
+ }
294
+
295
+ func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, userAnswer *models.UserAnswer) error {
296
+ return r.db.Save(userAnswer).Error
297
+ }
298
+
299
+ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
300
+ var resultJSON string
301
+ var reviewQuizResponse models.ReviewQuizResponse
302
+
303
+ reviews := make([]models.ReviewQuiz, 0)
304
+
305
+ rawQuery := `
306
+ SELECT
307
+ COALESCE(
308
+ json_agg(
309
+ json_build_object(
310
+ 'question', json_build_object(
311
+ 'id', q.id,
312
+ 'quiz_id', q.quiz_id,
313
+ 'content', q.content,
314
+ 'review', q.review
315
+ ),
316
+ 'answer_options', (
317
+ SELECT json_agg(
318
+ json_build_object(
319
+ 'id', a.id,
320
+ 'content', a.content,
321
+ 'is_correct', a.is_correct
322
+ )
323
+ ORDER BY a.id
324
+ )
325
+ FROM answers a
326
+ WHERE a.question_id = q.id
327
+ ),
328
+ 'answer_id', ua.selected_answer_id
329
+ ) ORDER BY q.order
330
+ ),
331
+ '[]'::json
332
+ ) AS reviews
333
+ FROM
334
+ questions q
335
+ LEFT JOIN
336
+ user_answers ua
337
+ ON q.id = ua.question_id
338
+ AND ua.quiz_attempt_id = ?
339
+ WHERE
340
+ q.quiz_id = ?
341
+ `
342
+
343
+ err := r.db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
344
+ if err != nil {
345
+ return nil, fmt.Errorf("failed to query reviews: %w", err)
346
+ }
347
+
348
+ // Unmarshal the JSON result into the Reviews field
349
+ err = json.Unmarshal([]byte(resultJSON), &reviews)
350
+ if err != nil {
351
+ return nil, fmt.Errorf("failed to unmarshal JSON result: %w", err)
352
+ }
353
+
354
+ reviewQuizResponse.Reviews = reviews
355
+ return &reviewQuizResponse, nil
356
+ }
357
+
358
+ func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error) {
359
+ var attempt models.QuizAttempt
360
+
361
+ err := r.db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
362
+ if err != nil {
363
+ return nil, err
364
+ }
365
+
366
+ return &attempt, nil
367
+ }
368
+
369
+ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error) {
370
+ var correctOptionID int64
371
+
372
+ rawQuery := `
373
+ SELECT id
374
+ FROM answers
375
+ WHERE question_id = ?
376
+ AND is_correct = TRUE
377
+ LIMIT 1;
378
+ `
379
+
380
+ err := r.db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
381
+ if err != nil {
382
+ return 0, err
383
+ }
384
+
385
+ return correctOptionID, nil
386
+ }
387
+
388
+ func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error {
389
+ r.db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
390
+ Delete(&models.AcademyMaterialProgress{})
391
+
392
+ // Delete quiz attempts for the quiz associated with the academy
393
+ r.db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
394
+ Delete(&models.QuizAttempt{})
395
+
396
+ return nil
397
+ }
space/space/space/space/space/router/quiz_route.go CHANGED
@@ -7,8 +7,11 @@ import (
7
  func (s *Server) QuizRoute() {
8
  userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
- // :id is academy id
11
- userRouterGroup.GET("/:id", middleware.AuthUser, s.quizController.UserGetQuiz)
12
- userRouterGroup.POST("/:id/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
 
 
 
13
  }
14
  }
 
7
  func (s *Server) QuizRoute() {
8
  userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
+ userRouterGroup.GET("", middleware.AuthUser, s.quizController.UserGetQuiz)
11
+ userRouterGroup.POST("/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
12
+ userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
13
+ userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
14
+ userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
15
+ userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
16
  }
17
  }
space/space/space/space/space/services/quiz_service.go CHANGED
@@ -3,29 +3,26 @@ package services
3
  import (
4
  "context"
5
  "errors"
6
- "math/rand"
7
  "time"
8
 
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/validation"
11
  "api.qobiltu.id/repositories"
12
  "api.qobiltu.id/response"
 
13
  "gorm.io/gorm"
14
  )
15
 
16
  type QuizService interface {
17
  // === ADMIN ===
18
 
19
- // // === USER ===
20
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
  UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
-
23
- // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
24
- // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
25
- // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
- // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
27
- // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
28
- // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
29
  }
30
 
31
  type quizService struct {
@@ -48,6 +45,7 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
48
  }
49
 
50
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
 
51
  quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
52
  AccountID: req.AccountID,
53
  AcademyID: req.AcademyID,
@@ -58,66 +56,52 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
58
 
59
  quiz := &quizAttempt.Quiz
60
 
 
61
  existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
62
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
63
  return nil, response.HandleGormError(err, "Internal Server Error")
64
  }
65
 
 
66
  if existingAttempt != nil {
67
- return s.handleExistingAttempt(ctx, req, quiz, existingAttempt)
68
  }
69
 
70
  return s.handleNewAttempt(ctx, req, quiz)
71
  }
72
 
73
- func (s *quizService) handleExistingAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
  now := time.Now()
75
 
76
- if attempt.DueAt.Before(now) {
77
- if attempt.FinishedAt == nil {
78
- attempt.FinishedAt = &now
79
- attempt.Score = s.calculateQuizScore(ctx, attempt)
80
-
81
- if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
82
- return nil, response.HandleGormError(err, "Internal Server Error")
83
- }
84
-
85
- totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
86
- if err != nil {
87
- return nil, response.HandleGormError(err, "Internal Server Error")
88
- }
89
-
90
- if totalAttempts >= quiz.AttemptLimit {
91
- if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
92
- return nil, response.HandleGormError(err, "Internal Server Error")
93
- }
94
-
95
- // hapus juga semua attempt quiz by account id dan quiz id
96
- if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
97
- return nil, response.HandleGormError(err, "Internal Server Error")
98
- }
99
- }
100
-
101
- return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
102
- }
103
-
104
- return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
105
  }
106
 
 
107
  questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
108
  if err != nil {
109
  return nil, response.HandleGormError(err, "Internal Server Error")
110
  }
111
 
112
- questions = shuffleWithKey(questions, attempt.ID)
113
 
114
  return &models.UserAttemptQuizResponse{
115
- QuizAttempt: *attempt,
116
- Questions: questions,
 
 
 
 
 
 
117
  }, nil
118
  }
119
 
120
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
 
 
121
  totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
122
  if err != nil {
123
  return nil, response.HandleGormError(err, "Internal Server Error")
@@ -134,67 +118,202 @@ func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAtte
134
  return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
135
  }
136
 
137
- quizAttempt := models.QuizAttempt{
 
138
  AccountID: req.AccountID,
139
  QuizID: quiz.ID,
 
140
  StartedAt: time.Now(),
141
  DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
142
  }
143
 
144
- if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt); err != nil {
145
  return nil, response.HandleGormError(err, "Internal Server Error")
146
  }
147
 
148
- questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
 
149
  if err != nil {
150
  return nil, response.HandleGormError(err, "Internal Server Error")
151
  }
152
 
153
- questions = shuffleWithKey(questions, quizAttempt.ID)
154
 
155
  return &models.UserAttemptQuizResponse{
156
- QuizAttempt: quizAttempt,
157
- Questions: questions,
 
 
 
 
 
 
158
  }, nil
159
  }
160
 
161
- func (s *quizService) calculateQuizScore(ctx context.Context, attempt *models.QuizAttempt) float64 {
 
 
 
 
162
  // ambil total question dari quiz
163
  totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
164
  if err != nil {
165
- return 0
166
  }
167
 
168
  if totalQuestion == 0 {
169
- return 0
 
 
 
 
 
170
  }
171
 
172
  // ambil semua user answer yang is_correct nya true
173
  correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
174
  if err != nil {
175
- return 0
176
  }
177
 
178
- // hitung score nya
179
  score := float64(correctAnswer) / float64(totalQuestion) * 100
180
 
181
- return score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
- // shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
185
- // untuk memastikan hasil acak yang konsisten untuk key yang sama
186
- func shuffleWithKey[T any](slice []T, key int64) []T {
187
- // Buat salinan slice untuk menghindari modifikasi original
188
- shuffled := make([]T, len(slice))
189
- copy(shuffled, slice)
190
 
191
- // Buat random source dengan seed dari key
192
- r := rand.New(rand.NewSource(key))
 
 
193
 
194
- // Lakukan shuffling
195
- r.Shuffle(len(shuffled), func(i, j int) {
196
- shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
197
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- return shuffled
200
  }
 
3
  import (
4
  "context"
5
  "errors"
 
6
  "time"
7
 
8
  "api.qobiltu.id/models"
9
  "api.qobiltu.id/pkg/validation"
10
  "api.qobiltu.id/repositories"
11
  "api.qobiltu.id/response"
12
+ "api.qobiltu.id/utils"
13
  "gorm.io/gorm"
14
  )
15
 
16
  type QuizService interface {
17
  // === ADMIN ===
18
 
19
+ // === USER ===
20
  UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
  UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
+ UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
23
+ UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
24
+ UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
25
+ UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
 
 
 
26
  }
27
 
28
  type quizService struct {
 
45
  }
46
 
47
  func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
48
+ // ambil data quiz attempt
49
  quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
50
  AccountID: req.AccountID,
51
  AcademyID: req.AcademyID,
 
56
 
57
  quiz := &quizAttempt.Quiz
58
 
59
+ // ambil attempt quiz yg sedang aktif
60
  existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
61
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
62
  return nil, response.HandleGormError(err, "Internal Server Error")
63
  }
64
 
65
+ // jika ada attempt yang sedang active
66
  if existingAttempt != nil {
67
+ return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
68
  }
69
 
70
  return s.handleNewAttempt(ctx, req, quiz)
71
  }
72
 
73
+ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
  now := time.Now()
75
 
76
+ // jika attempt nya telah melewati batas waktu dan belum submit / finish
77
+ if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
78
+ // frontend nge-trigger submit
79
+ return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
82
+ // jika masih dalam waktu yang ditentukan
83
  questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
84
  if err != nil {
85
  return nil, response.HandleGormError(err, "Internal Server Error")
86
  }
87
 
88
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
89
 
90
  return &models.UserAttemptQuizResponse{
91
+ ID: attempt.ID,
92
+ AccountID: attempt.AccountID,
93
+ QuizID: attempt.QuizID,
94
+ StartedAt: attempt.StartedAt,
95
+ DueAt: attempt.DueAt,
96
+ FinishedAt: attempt.FinishedAt,
97
+ Score: attempt.Score,
98
+ Questions: questions,
99
  }, nil
100
  }
101
 
102
  func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
103
+
104
+ // ini sekedar untuk make sure apakah masih bisa attemp dan telah membaca semua materi.
105
  totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
106
  if err != nil {
107
  return nil, response.HandleGormError(err, "Internal Server Error")
 
118
  return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
119
  }
120
 
121
+ // buat attempt quiz
122
+ attempt := models.QuizAttempt{
123
  AccountID: req.AccountID,
124
  QuizID: quiz.ID,
125
+ AcademyID: quiz.AcademyID,
126
  StartedAt: time.Now(),
127
  DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
128
  }
129
 
130
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &attempt); err != nil {
131
  return nil, response.HandleGormError(err, "Internal Server Error")
132
  }
133
 
134
+ // ambil question
135
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
136
  if err != nil {
137
  return nil, response.HandleGormError(err, "Internal Server Error")
138
  }
139
 
140
+ questions = utils.ShuffleWithKey(questions, attempt.ID)
141
 
142
  return &models.UserAttemptQuizResponse{
143
+ ID: attempt.ID,
144
+ AccountID: attempt.AccountID,
145
+ QuizID: attempt.QuizID,
146
+ StartedAt: attempt.StartedAt,
147
+ DueAt: attempt.DueAt,
148
+ FinishedAt: attempt.FinishedAt,
149
+ Score: attempt.Score,
150
+ Questions: questions,
151
  }, nil
152
  }
153
 
154
+ func (s *quizService) calculateQuizScore(
155
+ ctx context.Context,
156
+ quiz *models.Quiz,
157
+ attempt *models.QuizAttempt,
158
+ ) error {
159
  // ambil total question dari quiz
160
  totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
161
  if err != nil {
162
+ return err
163
  }
164
 
165
  if totalQuestion == 0 {
166
+ attempt.Score = 0
167
+ attempt.TotalQuestions = 0
168
+ attempt.TotalCorrectAnswer = 0
169
+ attempt.TotalWrongAnswer = 0
170
+ attempt.IsPassed = false
171
+ return nil
172
  }
173
 
174
  // ambil semua user answer yang is_correct nya true
175
  correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
176
  if err != nil {
177
+ return err
178
  }
179
 
 
180
  score := float64(correctAnswer) / float64(totalQuestion) * 100
181
 
182
+ attempt.Score = score
183
+ attempt.TotalQuestions = totalQuestion
184
+ attempt.TotalCorrectAnswer = correctAnswer
185
+ attempt.TotalWrongAnswer = totalQuestion - correctAnswer
186
+ attempt.IsPassed = score >= float64(quiz.MinScore)
187
+
188
+ now := time.Now()
189
+ attempt.FinishedAt = &now
190
+
191
+ return nil
192
+ }
193
+
194
+ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
195
+ // pastiin dulu attemp nya ada atau tidak
196
+ _, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
197
+ if err != nil {
198
+ return nil, response.HandleGormError(err, "Internal Server Error")
199
+ }
200
+
201
+ // ambil pertanyaan dan jawaban pengguna
202
+ question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
203
+ if err != nil {
204
+ if errors.Is(err, gorm.ErrRecordNotFound) {
205
+ return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
206
+ }
207
+ return nil, response.HandleGormError(err, "Internal Server Error")
208
+ }
209
+
210
+ return question, nil
211
  }
212
 
213
+ func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
 
 
 
 
 
214
 
215
+ correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, req.QuestionID)
216
+ if err != nil {
217
+ return nil, response.HandleGormError(err, "Internal Server Error")
218
+ }
219
 
220
+ question, err := s.quizRepository.UserGetUserAnswer(ctx, req.AttemptID, req.QuestionID)
221
+ if err != nil {
222
+
223
+ // jika belum ada answer
224
+ if errors.Is(err, gorm.ErrRecordNotFound) {
225
+ userAnswer := models.UserAnswer{
226
+ QuizAttemptID: req.AttemptID,
227
+ QuestionID: req.QuestionID,
228
+ AnswerID: req.AnswerID,
229
+ IsDoubt: req.IsDoubt,
230
+ IsCorrect: false,
231
+ }
232
+
233
+ if req.AnswerID != nil {
234
+ userAnswer.IsCorrect = *req.AnswerID == correctOptionID
235
+ }
236
+
237
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
238
+ return nil, response.HandleGormError(err, "Internal Server Error")
239
+ }
240
+
241
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
242
+ if err != nil {
243
+ return nil, response.HandleGormError(err, "Internal Server Error")
244
+ }
245
+
246
+ return res, nil
247
+ }
248
+
249
+ return nil, response.HandleGormError(err, "Internal Server Error")
250
+ }
251
+
252
+ // updating answer
253
+ question.AnswerID = req.AnswerID
254
+ question.IsDoubt = req.IsDoubt
255
+
256
+ if req.AnswerID != nil {
257
+ question.IsCorrect = *req.AnswerID == correctOptionID
258
+ }
259
+
260
+ if err := s.quizRepository.UserSaveUserAnswer(ctx, question); err != nil {
261
+ return nil, response.HandleGormError(err, "Internal Server Error")
262
+ }
263
+
264
+ res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
265
+ if err != nil {
266
+ if errors.Is(err, gorm.ErrRecordNotFound) {
267
+ return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
268
+ }
269
+ return nil, response.HandleGormError(err, "Internal Server Error")
270
+ }
271
+
272
+ return res, nil
273
+ }
274
+
275
+ func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
276
+ attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
277
+ if err != nil {
278
+ return nil, response.HandleGormError(err, "Internal Server Error")
279
+ }
280
+
281
+ if err := s.calculateQuizScore(ctx, attempt.Quiz, attempt); err != nil {
282
+ return nil, response.HandleGormError(err, "Internal Server Error")
283
+ }
284
+
285
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
286
+ return nil, response.HandleGormError(err, "Internal Server Error")
287
+ }
288
+
289
+ remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
290
+ if err != nil {
291
+ return nil, response.HandleGormError(err, "Internal Server Error")
292
+ }
293
+
294
+ if remainingAttempts == 0 {
295
+ // hapus progress dan semua attempt pada quiz
296
+ if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
297
+ return nil, response.HandleGormError(err, "Internal Server Error")
298
+ }
299
+ }
300
+
301
+ return &models.SubmitQuizResponse{
302
+ QuizAttempt: *attempt,
303
+ RemainingAttempts: remainingAttempts,
304
+ }, nil
305
+ }
306
+
307
+ func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
308
+ attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
309
+ if err != nil {
310
+ return nil, response.HandleGormError(err, "Internal Server Error")
311
+ }
312
+
313
+ review, err := s.quizRepository.UserGetReviewQuiz(ctx, attempt.ID, attempt.AccountID, attempt.QuizID)
314
+ if err != nil {
315
+ return nil, response.HandleGormError(err, "Internal Server Error")
316
+ }
317
 
318
+ return review, nil
319
  }
space/space/space/space/space/space/response/api_response_v2.go CHANGED
@@ -41,6 +41,15 @@ func HandleError(c *gin.Context, err error) {
41
  responseError(c, http.StatusRequestTimeout, exception)
42
  case exception.AttemptNotFound:
43
  responseError(c, http.StatusNotFound, exception)
 
 
 
 
 
 
 
 
 
44
  case exception.ValidationError:
45
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
46
  default:
 
41
  responseError(c, http.StatusRequestTimeout, exception)
42
  case exception.AttemptNotFound:
43
  responseError(c, http.StatusNotFound, exception)
44
+ case exception.QuizTimeExpired:
45
+ responseError(c, http.StatusBadRequest, exception)
46
+ case exception.QuizAlreadyFinished:
47
+ responseError(c, http.StatusBadRequest, exception)
48
+ case exception.QuizAttemptLimit:
49
+ responseError(c, http.StatusBadRequest, exception)
50
+ case exception.AcademyNotFinished:
51
+ responseError(c, http.StatusBadRequest, exception)
52
+
53
  case exception.ValidationError:
54
  responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
55
  default:
space/space/space/space/space/space/space/config/database_connection_config.go CHANGED
@@ -62,11 +62,13 @@ func AutoMigrateAll(db *gorm.DB) {
62
  &models.RegionProvince{},
63
  &models.OptionCategory{},
64
  &models.OptionValues{},
 
65
  &models.Quiz{},
66
- &models.QuizAttempt{},
67
  &models.Question{},
68
  &models.Answer{},
 
69
  &models.UserAnswer{},
 
70
  &models.PersonalityAndPreferenceCV{},
71
  &models.FamilyMemberCV{},
72
  &models.PhysicalAndHealthCV{},
 
62
  &models.RegionProvince{},
63
  &models.OptionCategory{},
64
  &models.OptionValues{},
65
+
66
  &models.Quiz{},
 
67
  &models.Question{},
68
  &models.Answer{},
69
+ &models.QuizAttempt{},
70
  &models.UserAnswer{},
71
+
72
  &models.PersonalityAndPreferenceCV{},
73
  &models.FamilyMemberCV{},
74
  &models.PhysicalAndHealthCV{},
space/space/space/space/space/space/space/controller/quiz/quiz_controller.go ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package quiz_controller
2
+
3
+ import (
4
+ "net/http"
5
+ "strconv"
6
+
7
+ "api.qobiltu.id/middleware"
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/response"
10
+ "api.qobiltu.id/services"
11
+ "github.com/gin-gonic/gin"
12
+ )
13
+
14
+ type QuizController interface {
15
+ // === ADMIN ===
16
+
17
+ // === USER ===
18
+ UserGetQuiz(ctx *gin.Context)
19
+ UserAttemptQuiz(ctx *gin.Context)
20
+ }
21
+
22
+ type quizController struct {
23
+ quizService services.QuizService
24
+ }
25
+
26
+ func NewQuizController(quizService services.QuizService) QuizController {
27
+ return &quizController{
28
+ quizService: quizService,
29
+ }
30
+ }
31
+
32
+ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
33
+ academyID := ctx.Param("id")
34
+ academyIDInt, err := strconv.Atoi(academyID)
35
+ if err != nil {
36
+ response.HandleError(ctx, err)
37
+ return
38
+ }
39
+
40
+ accountData := middleware.GetAccountData(ctx)
41
+ req := models.UserGetQuizRequest{
42
+ AccountID: int64(accountData.UserID),
43
+ AcademyID: int64(academyIDInt),
44
+ }
45
+
46
+ res, err := c.quizService.UserGetQuiz(ctx, &req)
47
+ if err != nil {
48
+ response.HandleError(ctx, err)
49
+ return
50
+ }
51
+
52
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", http.StatusOK, res)
53
+ }
54
+
55
+ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
56
+ academyID := ctx.Param("id")
57
+ academyIDInt, err := strconv.Atoi(academyID)
58
+ if err != nil {
59
+ response.HandleError(ctx, err)
60
+ return
61
+ }
62
+
63
+ accountData := middleware.GetAccountData(ctx)
64
+ req := models.UserAttemptQuizRequest{
65
+ AccountID: int64(accountData.UserID),
66
+ AcademyID: int64(academyIDInt),
67
+ }
68
+
69
+ res, err := c.quizService.UserAttemptQuiz(ctx, &req)
70
+ if err != nil {
71
+ response.HandleError(ctx, err)
72
+ return
73
+ }
74
+
75
+ response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", http.StatusOK, res)
76
+ }
space/space/space/space/space/space/space/main.go CHANGED
@@ -12,6 +12,7 @@ import (
12
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
13
  options_controller "api.qobiltu.id/controller/options"
14
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
 
15
  region_controller "api.qobiltu.id/controller/region"
16
  "api.qobiltu.id/pkg/mail"
17
  "api.qobiltu.id/pkg/storage"
@@ -75,6 +76,10 @@ func main() {
75
  academyService := services.NewAcademyService(academyRepository, validator)
76
  academyController := academy_controller.NewAcademyController(academyService)
77
 
 
 
 
 
78
  cvRepository := repositories.NewCVRepository(config.DB)
79
  cvService := services.NewCVService(cvRepository, localStorage, validator)
80
  cvController := cv_controller.NewCVController(cvService)
@@ -98,6 +103,7 @@ func main() {
98
  optionController,
99
  emailController,
100
  academyController,
 
101
  cvController,
102
  marriageReadinessProfileController,
103
  partnerCriteriaController,
 
12
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
13
  options_controller "api.qobiltu.id/controller/options"
14
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
15
+ quiz_controller "api.qobiltu.id/controller/quiz"
16
  region_controller "api.qobiltu.id/controller/region"
17
  "api.qobiltu.id/pkg/mail"
18
  "api.qobiltu.id/pkg/storage"
 
76
  academyService := services.NewAcademyService(academyRepository, validator)
77
  academyController := academy_controller.NewAcademyController(academyService)
78
 
79
+ quizRepository := repositories.NewQuizRepository(config.DB)
80
+ quizService := services.NewQuizService(quizRepository, academyRepository, validator)
81
+ quizController := quiz_controller.NewQuizController(quizService)
82
+
83
  cvRepository := repositories.NewCVRepository(config.DB)
84
  cvService := services.NewCVService(cvRepository, localStorage, validator)
85
  cvController := cv_controller.NewCVController(cvService)
 
103
  optionController,
104
  emailController,
105
  academyController,
106
+ quizController,
107
  cvController,
108
  marriageReadinessProfileController,
109
  partnerCriteriaController,
space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -134,57 +134,71 @@ type RegionCity struct {
134
  FullCode string `json:"full_code"`
135
  ProvinceID uint `json:"province_id"`
136
  }
137
- type Answer struct {
138
- ID uint `gorm:"primaryKey" json:"id"`
139
- QuestionID uint `json:"question_id"`
140
- Content string `json:"content"`
141
- IsCorrect bool `json:"-"`
142
- }
143
- type Question struct {
144
- ID uint `gorm:"primaryKey" json:"id"`
145
- QuizID uint `json:"quiz_id"`
146
- Content string `json:"content"`
147
- Order int `json:"order"`
148
- CorrectAnswer uint `json:"correct_answer"`
149
- Review string `json:"reviews"`
150
- }
151
- type Quiz struct {
152
- ID uint `gorm:"primaryKey" json:"id"`
153
- AcademyID uint `json:"academy_id"`
154
- Slug string `json:"slug" gorm:"uniqueIndex" `
155
- Title string `json:"title"`
156
- Description string `json:"description"`
157
- TotalQuestions int `json:"total_questions"`
158
- AttemptLimit int `json:"attempt_limit"`
159
- TimeLimit int `json:"time_limit"`
160
- MinScore int `json:"min_score"`
161
- CreatedAt time.Time `json:"created_at"`
162
- }
163
 
164
- type QuizAttempt struct {
165
- ID uint `gorm:"primaryKey" json:"id"`
166
- AccountID uint `json:"user_id"`
167
- QuizID uint `json:"quiz_id"`
168
- StartedAt time.Time `json:"started_at"`
169
- DueAt time.Time `json:"due_at"`
170
- FinishedAt *time.Time `json:"finished_at"`
171
- Score float64 `json:"score"`
172
- }
173
- type UserAnswer struct {
174
- ID uint `gorm:"primaryKey" json:"id"`
175
- QuizAttemptID uint `json:"quiz_attempt_id"`
176
- QuestionID uint `json:"question_id"`
177
- SelectedAnswer uint `json:"selected_answer"`
178
- IsDoubt bool `json:"id_doubt"`
179
- IsCorrect bool `json:"is_correct"`
180
- }
181
- type QuizResult struct {
182
- QuizAttemptID uint `gorm:"column:quiz_attempt_id" json:"quiz_attempt_id"`
183
- TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
184
- CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
185
- AverageScore float64 `gorm:"column:average_score" json:"average_score"`
186
- IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
187
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  type (
190
  PersonalityAndPreferenceCV struct {
@@ -243,27 +257,34 @@ type (
243
  }
244
 
245
  WorshipAndReligiousUnderstandingCV struct {
246
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
247
- AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
248
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
 
 
249
  ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
250
  CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
251
  TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
252
  DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
253
- QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
254
- QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
255
- WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // kajian_yang_diikuti_dalam_sepekan
256
  DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
257
  AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
 
 
 
258
  HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
259
- ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
260
- OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
261
- OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
262
- OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
263
- WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
264
- FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
265
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
266
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
 
 
 
 
 
267
  FieldCounter
268
  }
269
 
@@ -335,6 +356,46 @@ func (w WorshipAndReligiousUnderstandingCV) GetFilledFields() []string {
335
  return w.FieldCounter.GetFilledFields(w)
336
  }
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  type (
339
  MarriageReadinessProfile struct {
340
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
@@ -431,9 +492,9 @@ func (AcademyMaterial) TableName() string { return "academy_materials
431
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
432
  func (RegionProvince) TableName() string { return "region_provinces" }
433
  func (RegionCity) TableName() string { return "region_cities" }
434
- func (Answer) TableName() string { return "answers" }
435
- func (Question) TableName() string { return "questions" }
436
  func (Quiz) TableName() string { return "quizzes" }
 
 
437
  func (QuizAttempt) TableName() string { return "quiz_attempts" }
438
  func (UserAnswer) TableName() string { return "user_answers" }
439
  func (OptionCategory) TableName() string { return "option_categories" }
 
134
  FullCode string `json:"full_code"`
135
  ProvinceID uint `json:"province_id"`
136
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ type (
139
+ Quiz struct {
140
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
141
+ AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
142
+ Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
143
+ Slug string `gorm:"column:slug;uniqueIndex" json:"slug" `
144
+ Title string `gorm:"column:title" json:"title"`
145
+ Description string `gorm:"column:description" json:"description"`
146
+ AttemptLimit int64 `gorm:"column:attempt_limit" json:"attempt_limit"`
147
+ TimeLimit int64 `gorm:"column:time_limit" json:"time_limit"`
148
+ MinScore int64 `gorm:"column:min_score" json:"min_score"`
149
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
150
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
151
+ }
152
+
153
+ Question struct {
154
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
155
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
156
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
157
+ Content string `gorm:"column:content" json:"content"`
158
+ Order int `gorm:"column:order" json:"order"`
159
+ Review string `gorm:"column:reviews" json:"reviews"`
160
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
161
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
162
+ }
163
+
164
+ Answer struct {
165
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
166
+ QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
167
+ Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
168
+ Content string `gorm:"column:content" json:"content"`
169
+ IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
170
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
171
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
172
+ }
173
+
174
+ QuizAttempt struct {
175
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
176
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
177
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
178
+ QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
179
+ Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
180
+ StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
181
+ DueAt time.Time `gorm:"column:due_at" json:"due_at"`
182
+ FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
183
+ Score float64 `gorm:"column:score" json:"score"`
184
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
185
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
186
+ }
187
+
188
+ UserAnswer struct {
189
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
190
+ QuizAttemptID int64 `gorm:"column:quiz_attempt_id;not null" json:"quiz_attempt_id"`
191
+ QuizAttempt *QuizAttempt `gorm:"foreignKey:QuizAttemptID;constraint:OnDelete:CASCADE" json:"quiz_attempt,omitempty"`
192
+ QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
193
+ Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
194
+ AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
195
+ Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
196
+ IsDoubt bool `gorm:"column:id_doubt" json:"id_doubt"`
197
+ IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
198
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
199
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
200
+ }
201
+ )
202
 
203
  type (
204
  PersonalityAndPreferenceCV struct {
 
257
  }
258
 
259
  WorshipAndReligiousUnderstandingCV struct {
260
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
261
+ AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
262
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
263
+
264
+ // Data ibadah
265
  ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
266
  CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
267
  TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
268
  DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
 
 
 
269
  DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
270
  AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
271
+ QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
272
+ QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
273
+ WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // jumlah kajian yang diikuti dalam seminggu
274
  HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
275
+
276
+ // Data Pemahaman Agama
277
+ ListeningToMusic *bool `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
278
+ OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
279
+ OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
280
+ OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
281
+ OpinionOnBeard *string `gorm:"column:opinion_on_beard" json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
282
+ OpinionOnPantsAboveAnkle *string `gorm:"column:opinion_on_pants_above_ankle" json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
283
+ WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
284
+ FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
285
+
286
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
287
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
288
  FieldCounter
289
  }
290
 
 
356
  return w.FieldCounter.GetFilledFields(w)
357
  }
358
 
359
+ var WorshipFields = []string{
360
+ "ObligatoryPrayer",
361
+ "CongregationalPrayer",
362
+ "TahajjudPrayer",
363
+ "DhuhaPrayer",
364
+ "DaudFasting",
365
+ "AyyamulBidhFasting",
366
+ "QuranReadingAbility",
367
+ "QuranMemorization",
368
+ "WeeklyReligiousStudyFrequency",
369
+ "HajjOrUmrah",
370
+ }
371
+
372
+ var ReligiousUnderstandingFields = []string{
373
+ "ListeningToMusic",
374
+ "OpinionOnIkhtilat",
375
+ "OpinionOnTouchingNonMahram",
376
+ "OpinionOnVeil",
377
+ "OpinionOnBeard",
378
+ "OpinionOnPantsAboveAnkle",
379
+ "WeeklyReligiousStudies",
380
+ "FollowedUstadz",
381
+ }
382
+
383
+ func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsWorship() int {
384
+ return w.FieldCounter.CountFieldsByNames(w, WorshipFields)
385
+ }
386
+
387
+ func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsReligiousUnderstanding() int {
388
+ return w.FieldCounter.CountFieldsByNames(w, ReligiousUnderstandingFields)
389
+ }
390
+
391
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsWorship() []string {
392
+ return w.FieldCounter.GetFilledFieldsByNames(w, WorshipFields)
393
+ }
394
+
395
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsReligiousUnderstanding() []string {
396
+ return w.FieldCounter.GetFilledFieldsByNames(w, ReligiousUnderstandingFields)
397
+ }
398
+
399
  type (
400
  MarriageReadinessProfile struct {
401
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
 
492
  func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
493
  func (RegionProvince) TableName() string { return "region_provinces" }
494
  func (RegionCity) TableName() string { return "region_cities" }
 
 
495
  func (Quiz) TableName() string { return "quizzes" }
496
+ func (Question) TableName() string { return "questions" }
497
+ func (Answer) TableName() string { return "answers" }
498
  func (QuizAttempt) TableName() string { return "quiz_attempts" }
499
  func (UserAnswer) TableName() string { return "user_answers" }
500
  func (OptionCategory) TableName() string { return "option_categories" }
space/space/space/space/space/space/space/models/exception_model.go CHANGED
@@ -18,6 +18,12 @@ type Exception struct {
18
  Forbidden bool `json:"forbidden,omitempty"`
19
  ValidationError bool `json:"validation_error,omitempty"`
20
 
 
 
 
 
 
 
21
  Message string `json:"message,omitempty"`
22
  Err error `json:"-"`
23
  ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
 
18
  Forbidden bool `json:"forbidden,omitempty"`
19
  ValidationError bool `json:"validation_error,omitempty"`
20
 
21
+ // quiz context
22
+ QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
23
+ QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
24
+ QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
25
+ AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
26
+
27
  Message string `json:"message,omitempty"`
28
  Err error `json:"-"`
29
  ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
space/space/space/space/space/space/space/models/field_counter.go CHANGED
@@ -97,6 +97,46 @@ func (fc FieldCounter) GetFilledFields(s any) []string {
97
  return filledFields
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  // isFieldFilled memeriksa apakah field memiliki nilai non-zero
101
  func isFieldFilled(field reflect.Value) bool {
102
  // Jika field tidak dapat di-address atau diakses, anggap kosong
 
97
  return filledFields
98
  }
99
 
100
+ // GetFilledFieldsByNames mengembalikan field terisi berdasarkan nama field yang diberikan
101
+ func (fc FieldCounter) GetFilledFieldsByNames(s any, fieldNames []string) []string {
102
+ var filledFields []string
103
+ v := reflect.ValueOf(s)
104
+
105
+ if v.Kind() == reflect.Ptr {
106
+ if v.IsNil() {
107
+ return filledFields
108
+ }
109
+ v = v.Elem()
110
+ }
111
+
112
+ t := v.Type()
113
+ fieldNameSet := make(map[string]struct{}, len(fieldNames))
114
+ for _, name := range fieldNames {
115
+ fieldNameSet[name] = struct{}{}
116
+ }
117
+
118
+ for i := 0; i < v.NumField(); i++ {
119
+ field := v.Field(i)
120
+ fieldType := t.Field(i)
121
+ name := fieldType.Name
122
+
123
+ if shouldSkipField(fieldType) {
124
+ continue
125
+ }
126
+
127
+ if _, ok := fieldNameSet[name]; ok && isFieldFilled(field) {
128
+ filledFields = append(filledFields, name)
129
+ }
130
+ }
131
+
132
+ return filledFields
133
+ }
134
+
135
+ // CountFieldsByNames menghitung total field dari daftar yang diberikan (dianggap valid dan bukan skip)
136
+ func (fc FieldCounter) CountFieldsByNames(s any, fieldNames []string) int {
137
+ return len(fieldNames)
138
+ }
139
+
140
  // isFieldFilled memeriksa apakah field memiliki nilai non-zero
141
  func isFieldFilled(field reflect.Value) bool {
142
  // Jika field tidak dapat di-address atau diakses, anggap kosong
space/space/space/space/space/space/space/models/request_model.go CHANGED
@@ -152,22 +152,31 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
152
  }
153
 
154
  type (
155
- ListQuizRequest struct {
156
- AccountID int64 `json:"account_id"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
158
  }
159
 
160
- ListQuizResponse struct {
161
- Quiz []Quiz `json:"quiz"`
 
 
162
  }
163
 
164
- AttemptQuizRequest struct {
165
  AccountID int64 `json:"account_id"`
166
- QuizID int64 `json:"quiz_id" validate:"required"`
 
 
 
 
 
 
167
  }
168
 
169
- AttemptQuizResponse struct {
170
- QuizAttempt QuizAttempt `json:"quiz_attempt"`
 
171
  }
172
 
173
  GetQuestionQuizRequest struct {
@@ -356,10 +365,12 @@ type (
356
  DaudFasting *string `json:"daud_fasting"` // puasa_daud
357
  AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
358
  HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
359
- ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
360
  OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
361
  OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
362
  OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
 
 
363
  WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
364
  FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
365
  }
@@ -467,15 +478,16 @@ type (
467
  }
468
 
469
  GetProgressCVResponse struct {
470
- AccountDetailsProgress float64 `json:"account_details_progress"`
471
- PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
472
- FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
473
- PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
474
- WorshipAndReligiousUnderstandingCVProgress float64 `json:"worship_and_religious_understanding_cv_progress"`
475
- EducationCVProgress float64 `json:"education_cv_progress"`
476
- JobCVProgress float64 `json:"job_cv_progress"`
477
- AchievementCVProgress float64 `json:"achievement_cv_progress"`
478
- TotalProgress float64 `json:"total_progress"`
 
479
  }
480
  )
481
 
 
152
  }
153
 
154
  type (
155
+ UserGetQuizRequest struct {
156
+ AccountID int64 `json:"account_id" validate:"required"`
157
  AcademyID int64 `json:"academy_id" validate:"required"`
158
  }
159
 
160
+ UserGetQuizResponse struct {
161
+ Quiz
162
+ TotalQuestions int64 `json:"total_questions"`
163
+ UserAttempts int64 `json:"user_attempts"`
164
  }
165
 
166
+ UserAttemptQuizRequest struct {
167
  AccountID int64 `json:"account_id"`
168
+ AcademyID int64 `json:"academy_id"`
169
+ }
170
+
171
+ UserAttemptQuizQuestionsResponse struct {
172
+ ID int64 `gorm:"column:id" json:"id"`
173
+ IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
174
+ IsAnswered bool `gorm:"column:is_answered" json:"is_answered"`
175
  }
176
 
177
+ UserAttemptQuizResponse struct {
178
+ QuizAttempt
179
+ Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
180
  }
181
 
182
  GetQuestionQuizRequest struct {
 
365
  DaudFasting *string `json:"daud_fasting"` // puasa_daud
366
  AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
367
  HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
368
+ ListeningToMusic *bool `json:"listening_to_music"` // mendengarkan_musik
369
  OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
370
  OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
371
  OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
372
+ OpinionOnBeard *string `json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
373
+ OpinionOnPantsAboveAnkle *string `json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
374
  WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
375
  FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
376
  }
 
478
  }
479
 
480
  GetProgressCVResponse struct {
481
+ AccountDetailsProgress float64 `json:"account_details_progress"`
482
+ PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
483
+ FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
484
+ PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
485
+ WorshipCVProgress float64 `json:"worship_cv_progress"`
486
+ ReligiousUnderstandingCVProgress float64 `json:"religious_understanding_cv_progress"`
487
+ EducationCVProgress float64 `json:"education_cv_progress"`
488
+ JobCVProgress float64 `json:"job_cv_progress"`
489
+ AchievementCVProgress float64 `json:"achievement_cv_progress"`
490
+ TotalProgress float64 `json:"total_progress"`
491
  }
492
  )
493
 
space/space/space/space/space/space/space/models/response_model.go CHANGED
@@ -45,11 +45,6 @@ type QuestionResponse struct {
45
  IsDoubt bool `json:"is_doubt"`
46
  }
47
 
48
- type QuizResultResponse struct {
49
- QuizAttempt QuizAttempt `json:"quiz_attempt"`
50
- Result QuizResult `json:"result"`
51
- }
52
-
53
  type OnExamUserAnswerResponse struct {
54
  ID uint `gorm:"primaryKey" json:"id"`
55
  QuizAttemptID uint `json:"quiz_attempt_id"`
 
45
  IsDoubt bool `json:"is_doubt"`
46
  }
47
 
 
 
 
 
 
48
  type OnExamUserAnswerResponse struct {
49
  ID uint `gorm:"primaryKey" json:"id"`
50
  QuizAttemptID uint `json:"quiz_attempt_id"`
space/space/space/space/space/space/space/repositories/academy_repository.go CHANGED
@@ -26,12 +26,14 @@ type AcademyRepository interface {
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
 
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
 
35
  }
36
 
37
  type academyRepository struct {
@@ -286,6 +288,37 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
286
  return academies, pageInfo, nil
287
  }
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
290
  materials := make([]models.UserAcademyMaterialResponse, 0)
291
  offset := req.Filter.GetOffset()
@@ -414,3 +447,18 @@ func (r *academyRepository) UserGetAcademyMaterialProgressByAccountID(ctx contex
414
  func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
415
  return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
416
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  // === USER ===
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
+ UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error)
30
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
31
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
32
  UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
33
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
34
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
35
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
36
+ UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error
37
  }
38
 
39
  type academyRepository struct {
 
288
  return academies, pageInfo, nil
289
  }
290
 
291
+ func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error) {
292
+ var totalMaterial int64
293
+ var totalReadMaterial int64
294
+
295
+ // Hitung total materi dalam academy
296
+ err := r.db.WithContext(ctx).
297
+ Model(&models.AcademyMaterial{}).
298
+ Where("academy_id = ?", academyID).
299
+ Count(&totalMaterial).Error
300
+ if err != nil {
301
+ return 0, err
302
+ }
303
+
304
+ // Hitung total materi yang sudah dibaca oleh user
305
+ err = r.db.WithContext(ctx).
306
+ Model(&models.AcademyMaterialProgress{}).
307
+ Joins("JOIN academy_materials am ON am.id = academy_materials_progress.academy_material_id").
308
+ Where("academy_materials_progress.account_id = ? AND am.academy_id = ?", accountID, academyID).
309
+ Count(&totalReadMaterial).Error
310
+ if err != nil {
311
+ return 0, err
312
+ }
313
+
314
+ // Hitung persentase
315
+ if totalMaterial == 0 {
316
+ return 0, nil
317
+ }
318
+ percentage := float64(totalReadMaterial) / float64(totalMaterial) * 100
319
+ return percentage, nil
320
+ }
321
+
322
  func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
323
  materials := make([]models.UserAcademyMaterialResponse, 0)
324
  offset := req.Filter.GetOffset()
 
447
  func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
448
  return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
449
  }
450
+
451
+ func (r *academyRepository) UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error {
452
+ // Subquery untuk mendapatkan semua material ID dari academyID
453
+ subQuery := r.db.
454
+ WithContext(ctx).
455
+ Model(&models.AcademyMaterial{}).
456
+ Select("id").
457
+ Where("academy_id = ?", academyID)
458
+
459
+ // Delete progress berdasarkan account_id dan academy_material_id yang ada di subquery
460
+ return r.db.
461
+ WithContext(ctx).
462
+ Where("account_id = ? AND academy_material_id IN (?)", accountID, subQuery).
463
+ Delete(&models.AcademyMaterialProgress{}).Error
464
+ }
space/space/space/space/space/space/space/repositories/quiz_repository.go CHANGED
@@ -8,7 +8,15 @@ import (
8
  )
9
 
10
  type QuizRepository interface {
11
- UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
 
 
 
 
 
 
 
 
12
  }
13
 
14
  type quizRepository struct {
@@ -19,144 +27,129 @@ func NewQuizRepository(db *gorm.DB) QuizRepository {
19
  return &quizRepository{db: db}
20
  }
21
 
22
- func (r *quizRepository) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
23
- panic("not implemented")
24
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
27
- repo := Construct[models.Quiz, []models.Quiz](
28
- models.Quiz{AcademyID: academyId},
29
- )
30
- repo.Transactions(
31
- WhereGivenConstructor[models.Quiz, []models.Quiz],
32
- Find[models.Quiz, []models.Quiz],
33
- )
34
- return *repo
35
- }
36
 
37
- func GetAllUserAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, []models.QuizAttempt] {
38
- repo := Construct[models.QuizAttempt, []models.QuizAttempt](
39
- models.QuizAttempt{AccountID: userId, QuizID: quizId},
40
- )
41
- repo.Transactions(
42
- WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
43
- Find[models.QuizAttempt, []models.QuizAttempt],
44
- )
45
- return *repo
46
  }
47
 
48
- func GetQuizbyId(quizId uint) Repository[models.Quiz, models.Quiz] {
49
- repo := Construct[models.Quiz, models.Quiz](
50
- models.Quiz{ID: quizId},
51
- )
52
- repo.Transactions(
53
- WhereGivenConstructor[models.Quiz, models.Quiz],
54
- Find[models.Quiz, models.Quiz],
55
- )
56
 
57
- return *repo
58
- }
 
 
59
 
60
- func GetUserLastAttempt(userId uint, quizId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
61
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
62
- models.QuizAttempt{AccountID: userId, QuizID: quizId},
63
- )
64
- repo.Transaction.Where(&repo.Constructor).Last(&repo.Result)
65
- repo.RowsError = repo.Transaction.Error
66
- repo.NoRecord = false
67
- // fmt.Println(repo.Transaction.RowsAffected) Kenapa 0 !!!!
68
- return *repo
69
  }
70
 
71
- func GetAttemptByIdandUserId(attemptId uint, userId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
72
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
73
- models.QuizAttempt{
74
- ID: attemptId,
75
- AccountID: userId,
76
- },
77
- )
78
- repo.Transactions(
79
- WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
80
- Find[models.QuizAttempt, models.QuizAttempt],
81
- )
82
- return *repo
83
  }
84
 
85
- func GetAttemptByUserId(userId uint) Repository[models.QuizAttempt, []models.QuizAttempt] {
86
- repo := Construct[models.QuizAttempt, []models.QuizAttempt](
87
- models.QuizAttempt{
88
- AccountID: userId,
89
- },
90
- )
91
- repo.Transactions(
92
- WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
93
- Find[models.QuizAttempt, []models.QuizAttempt],
94
- )
95
- return *repo
96
- }
97
- func CreateAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
98
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
99
- quizAttempt,
100
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- Create(repo)
103
- return *repo
104
  }
105
 
106
- func GetUserAnswerByAttemptQuestionId(attemptId uint, questionId uint) Repository[models.UserAnswer, models.UserAnswer] {
107
- repo := Construct[models.UserAnswer, models.UserAnswer](
108
- models.UserAnswer{
109
- QuizAttemptID: attemptId,
110
- QuestionID: questionId,
111
- },
112
- )
113
- repo.Transactions(
114
- WhereGivenConstructor[models.UserAnswer, models.UserAnswer],
115
- Find[models.UserAnswer, models.UserAnswer],
116
- )
117
- return *repo
118
  }
119
 
120
- func CreateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
121
- repo := Construct[models.UserAnswer, models.UserAnswer](
122
- userAnswer,
123
- )
124
 
125
- Create(repo)
126
- return *repo
127
- }
 
128
 
129
- func UpdateUserAnswer(userAnswer models.UserAnswer) Repository[models.UserAnswer, models.UserAnswer] {
130
- repo := Construct[models.UserAnswer, models.UserAnswer](
131
- userAnswer,
132
- )
133
- Update(repo)
134
- return *repo
135
  }
136
 
137
- func UpdateQuizAttempt(quizAttempt models.QuizAttempt) Repository[models.QuizAttempt, models.QuizAttempt] {
138
- repo := Construct[models.QuizAttempt, models.QuizAttempt](
139
- quizAttempt,
140
- )
141
- Update(repo)
142
- return *repo
143
- }
144
 
145
- func CountUserAttemptScore(attemptId uint) Repository[models.QuizAttempt, models.QuizResult] {
146
- repo := Construct[models.QuizAttempt, models.QuizResult](
147
- models.QuizAttempt{ID: attemptId},
148
- )
149
- repo.Transaction.Model(&repo.Constructor).Raw("SELECT quiz_attempt_id,COUNT(*) AS total_questions,SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS correct_answers,CAST(SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) AS average_score FROM user_answers WHERE quiz_attempt_id = ? GROUP BY quiz_attempt_id", attemptId).Scan(&repo.Result)
150
- repo.RowsError = repo.Transaction.Error
151
- repo.NoRecord = false
152
- return *repo
153
  }
154
 
155
- func GetUserAnswerByAttemptId(attemptId uint) Repository[models.UserAnswer, []models.OnExamUserAnswerResponse] {
156
- repo := Construct[models.UserAnswer, []models.OnExamUserAnswerResponse](
157
- models.UserAnswer{QuizAttemptID: attemptId},
158
- )
 
 
 
 
 
 
159
 
160
- repo.Transaction.Model(&repo.Constructor).Find(&repo.Result)
161
- return *repo
162
  }
 
8
  )
9
 
10
  type QuizRepository interface {
11
+ UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
12
+ UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
13
+ UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
14
+ UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
15
+ UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
16
+ UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
17
+ UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
18
+ UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
19
+ UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
20
  }
21
 
22
  type quizRepository struct {
 
27
  return &quizRepository{db: db}
28
  }
29
 
30
+ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
31
+ var quizResponse models.UserGetQuizResponse
32
+
33
+ rawQuery := `
34
+ SELECT
35
+ q.*,
36
+ (SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
37
+ COALESCE(COUNT(qa.id), 0) AS user_attempts
38
+ FROM
39
+ quizzes q
40
+ LEFT JOIN
41
+ quiz_attempts qa
42
+ ON q.id = qa.quiz_id
43
+ AND qa.account_id = ?
44
+ WHERE
45
+ q.academy_id = ?
46
+ GROUP BY
47
+ q.id, q.academy_id, q.slug, q.title, q.description,
48
+ q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
49
+ `
50
 
51
+ err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
52
+ if err != nil {
53
+ return nil, err
54
+ }
 
 
 
 
 
 
55
 
56
+ if quizResponse.ID == 0 {
57
+ return nil, gorm.ErrRecordNotFound
58
+ }
59
+
60
+ return &quizResponse, nil
 
 
 
 
61
  }
62
 
63
+ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
64
+ var quizAttempt models.QuizAttempt
 
 
 
 
 
 
65
 
66
+ err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
 
71
+ return &quizAttempt, nil
 
 
 
 
 
 
 
 
72
  }
73
 
74
+ func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
75
+ return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
+ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
79
+ questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
80
+
81
+ rawQuery := `
82
+ SELECT
83
+ q.id,
84
+ COALESCE(ua.is_doubt, FALSE) AS is_doubt,
85
+ CASE
86
+ WHEN ua.id IS NOT NULL
87
+ AND EXISTS (
88
+ SELECT 1
89
+ FROM quiz_attempts qa
90
+ WHERE qa.id = ?
91
+ AND qa.account_id = ?
92
+ AND qa.quiz_id = ?
93
+ ) THEN TRUE
94
+ ELSE FALSE
95
+ END AS is_answered
96
+ FROM
97
+ questions q
98
+ LEFT JOIN
99
+ user_answers ua
100
+ ON q.id = ua.question_id
101
+ AND ua.quiz_attempt_id = ?
102
+ WHERE
103
+ q.quiz_id = ?
104
+ ORDER BY
105
+ q.id;
106
+ `
107
+
108
+ err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
109
+ if err != nil {
110
+ return nil, err
111
+ }
112
 
113
+ return questions, nil
 
114
  }
115
 
116
+ func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
117
+ return r.db.Create(attempt).Error
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
+ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
121
+ var totalAttempts int64
 
 
122
 
123
+ err := r.db.Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
124
+ if err != nil {
125
+ return 0, err
126
+ }
127
 
128
+ return totalAttempts, nil
 
 
 
 
 
129
  }
130
 
131
+ func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
132
+ var totalQuestion int64
133
+
134
+ err := r.db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
135
+ if err != nil {
136
+ return 0, err
137
+ }
138
 
139
+ return totalQuestion, nil
 
 
 
 
 
 
 
140
  }
141
 
142
+ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error) {
143
+ var totalCorrectAnswer int64
144
+
145
+ err := r.db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
146
+ if err != nil {
147
+ return 0, err
148
+ }
149
+
150
+ return totalCorrectAnswer, nil
151
+ }
152
 
153
+ func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
154
+ return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
155
  }
space/space/space/space/space/space/space/router/quiz_route.go CHANGED
@@ -1,22 +1,14 @@
1
  package router
2
 
3
  import (
4
- QuizController "api.qobiltu.id/controller/quiz"
5
  "api.qobiltu.id/middleware"
6
- "github.com/gin-gonic/gin"
7
  )
8
 
9
- func QuizRoute(router *gin.Engine) {
10
- routerGroup := router.Group("/api/v1/quiz")
11
  {
12
- routerGroup.GET("/:academy_id/list", middleware.AuthUser, QuizController.List)
13
- routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
14
- routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
15
- routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
- routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
- routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
- routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
19
- routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
- routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }
22
  }
 
1
  package router
2
 
3
  import (
 
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) QuizRoute() {
8
+ userRouterGroup := s.router.Group("/api/v1/quiz")
9
  {
10
+ // :id is academy id
11
+ userRouterGroup.GET("/:id", middleware.AuthUser, s.quizController.UserGetQuiz)
12
+ userRouterGroup.POST("/:id/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
 
 
 
 
 
 
13
  }
14
  }
space/space/space/space/space/space/space/router/router.go CHANGED
@@ -10,11 +10,11 @@ func (s *Server) setupRoutes() {
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
13
- QuizRoute(s.router)
14
 
15
  s.OptionsRoute()
16
  s.EmailRoute()
17
  s.AcademyRoute()
 
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
 
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
 
13
 
14
  s.OptionsRoute()
15
  s.EmailRoute()
16
  s.AcademyRoute()
17
+ s.QuizRoute()
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
space/space/space/space/space/space/space/router/server.go CHANGED
@@ -7,6 +7,7 @@ import (
7
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
8
  options_controller "api.qobiltu.id/controller/options"
9
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
 
10
  region_controller "api.qobiltu.id/controller/region"
11
  "github.com/gin-gonic/gin"
12
  )
@@ -17,6 +18,7 @@ type Server struct {
17
  optionsController options_controller.OptionsController
18
  emailController email_controller.EmailController
19
  academyController academy_controller.AcademyController
 
20
  cvController cv_controller.CVController
21
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
22
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
@@ -27,6 +29,7 @@ func NewServer(
27
  optionsController options_controller.OptionsController,
28
  emailController email_controller.EmailController,
29
  academyController academy_controller.AcademyController,
 
30
  cvController cv_controller.CVController,
31
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
32
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
@@ -40,6 +43,7 @@ func NewServer(
40
  optionsController: optionsController,
41
  emailController: emailController,
42
  academyController: academyController,
 
43
  cvController: cvController,
44
  marriageReadinessProfileController: marriageReadinessProfileController,
45
  partnerCriteriaController: partnerCriteriaController,
 
7
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
8
  options_controller "api.qobiltu.id/controller/options"
9
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
10
+ quiz_controller "api.qobiltu.id/controller/quiz"
11
  region_controller "api.qobiltu.id/controller/region"
12
  "github.com/gin-gonic/gin"
13
  )
 
18
  optionsController options_controller.OptionsController
19
  emailController email_controller.EmailController
20
  academyController academy_controller.AcademyController
21
+ quizController quiz_controller.QuizController
22
  cvController cv_controller.CVController
23
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
24
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
 
29
  optionsController options_controller.OptionsController,
30
  emailController email_controller.EmailController,
31
  academyController academy_controller.AcademyController,
32
+ quizController quiz_controller.QuizController,
33
  cvController cv_controller.CVController,
34
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
35
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
 
43
  optionsController: optionsController,
44
  emailController: emailController,
45
  academyController: academyController,
46
+ quizController: quizController,
47
  cvController: cvController,
48
  marriageReadinessProfileController: marriageReadinessProfileController,
49
  partnerCriteriaController: partnerCriteriaController,
space/space/space/space/space/space/space/services/cv_service.go CHANGED
@@ -3,6 +3,7 @@ package services
3
  import (
4
  "context"
5
  "errors"
 
6
  "math"
7
  "strconv"
8
  "strings"
@@ -318,6 +319,7 @@ func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhy
318
 
319
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
320
  if err := s.validator.Validate(req); err != nil {
 
321
  return nil, response.HandleValidationError(err)
322
  }
323
 
@@ -339,16 +341,19 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
339
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
340
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
341
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
342
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
343
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
344
- utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
345
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
346
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
 
 
 
347
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
 
348
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
349
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
350
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
351
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
 
 
352
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
353
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
354
 
@@ -694,24 +699,27 @@ func calculateProgress(
694
  accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
695
  personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
696
  physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
697
- worshipAndReligiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFields())) / float64(worshipAndReligiousUnderstandingCV.TotalFields()) * 100
 
 
698
 
699
  educationCVPercentage := fullIfPresent(educationCV)
700
  familyMemberCVPercentage := fullIfPresent(familyMemberCV)
701
  jobCVPercentage := fullIfPresent(jobCV)
702
  achievementCVPercentage := fullIfPresent(achievementCV)
703
 
704
- overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipAndReligiousUnderstandingCVPercentage + educationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 8
705
 
706
  return &models.GetProgressCVResponse{
707
- AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
708
- PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
709
- FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
710
- PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
711
- WorshipAndReligiousUnderstandingCVProgress: math.Round(worshipAndReligiousUnderstandingCVPercentage*100) / 100,
712
- EducationCVProgress: math.Round(educationCVPercentage*100) / 100,
713
- JobCVProgress: math.Round(jobCVPercentage*100) / 100,
714
- AchievementCVProgress: math.Round(achievementCVPercentage*100) / 100,
715
- TotalProgress: math.Round(overallProgress*100) / 100,
 
716
  }
717
  }
 
3
  import (
4
  "context"
5
  "errors"
6
+ "fmt"
7
  "math"
8
  "strconv"
9
  "strings"
 
319
 
320
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
321
  if err := s.validator.Validate(req); err != nil {
322
+ fmt.Println(err)
323
  return nil, response.HandleValidationError(err)
324
  }
325
 
 
341
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
342
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
343
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
 
 
 
344
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
345
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
346
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
347
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
348
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
349
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
350
+
351
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
352
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
353
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
354
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
355
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnBeard, req.OpinionOnBeard)
356
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnPantsAboveAnkle, req.OpinionOnPantsAboveAnkle)
357
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
358
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
359
 
 
699
  accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
700
  personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
701
  physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
702
+
703
+ worshipCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsWorship())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsWorship()) * 100
704
+ religiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsReligiousUnderstanding())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsReligiousUnderstanding()) * 100
705
 
706
  educationCVPercentage := fullIfPresent(educationCV)
707
  familyMemberCVPercentage := fullIfPresent(familyMemberCV)
708
  jobCVPercentage := fullIfPresent(jobCV)
709
  achievementCVPercentage := fullIfPresent(achievementCV)
710
 
711
+ overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipCVPercentage + religiousUnderstandingCVPercentage + educationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 9
712
 
713
  return &models.GetProgressCVResponse{
714
+ AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
715
+ PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
716
+ FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
717
+ PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
718
+ WorshipCVProgress: math.Round(worshipCVPercentage*100) / 100,
719
+ ReligiousUnderstandingCVProgress: math.Round(religiousUnderstandingCVPercentage*100) / 100,
720
+ EducationCVProgress: math.Round(educationCVPercentage*100) / 100,
721
+ JobCVProgress: math.Round(jobCVPercentage*100) / 100,
722
+ AchievementCVProgress: math.Round(achievementCVPercentage*100) / 100,
723
+ TotalProgress: math.Round(overallProgress*100) / 100,
724
  }
725
  }
space/space/space/space/space/space/space/services/quiz_service.go CHANGED
@@ -1,37 +1,200 @@
1
  package services
2
 
3
- // type QuizService interface {
4
- // // === ADMIN ===
5
-
6
- // // === USER ===
7
- // UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
8
- // UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error)
9
- // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
10
- // // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
11
- // // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
12
- // // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
13
- // // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
14
- // // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
15
- // }
16
-
17
- // type quizService struct {
18
- // quizRepository repositories.QuizRepository
19
- // validator *validation.Validator
20
- // }
21
-
22
- // func NewQuizService(quizRepository repositories.QuizRepository, validator *validation.Validator) QuizService {
23
- // return &quizService{quizRepository: quizRepository, validator: validator}
24
- // }
25
-
26
- // func (s *quizService) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
27
- // quiz, paging, err := s.quizRepository.UserListQuiz(ctx, req)
28
- // if err != nil {
29
- // return nil, nil, response.HandleGormError(err, "Internal Server Error")
30
- // }
31
-
32
- // return quiz, paging, nil
33
- // }
34
-
35
- // func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.AttemptQuizRequest) (*models.AttemptQuizResponse, error) {
36
- // panic("not implemented")
37
- // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package services
2
 
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "math/rand"
7
+ "time"
8
+
9
+ "api.qobiltu.id/models"
10
+ "api.qobiltu.id/pkg/validation"
11
+ "api.qobiltu.id/repositories"
12
+ "api.qobiltu.id/response"
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ type QuizService interface {
17
+ // === ADMIN ===
18
+
19
+ // // === USER ===
20
+ UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
21
+ UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
22
+
23
+ // UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
24
+ // UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
25
+ // UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
26
+ // UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
27
+ // UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
28
+ // UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
29
+ }
30
+
31
+ type quizService struct {
32
+ quizRepository repositories.QuizRepository
33
+ academyRepository repositories.AcademyRepository
34
+ validator *validation.Validator
35
+ }
36
+
37
+ func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
38
+ return &quizService{quizRepository: quizRepository, academyRepository: academyRepository, validator: validator}
39
+ }
40
+
41
+ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
42
+ quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
43
+ if err != nil {
44
+ return nil, response.HandleGormError(err, "Internal Server Error")
45
+ }
46
+
47
+ return quiz, nil
48
+ }
49
+
50
+ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
51
+ quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
52
+ AccountID: req.AccountID,
53
+ AcademyID: req.AcademyID,
54
+ })
55
+ if err != nil {
56
+ return nil, response.HandleGormError(err, "Internal Server Error")
57
+ }
58
+
59
+ quiz := &quizAttempt.Quiz
60
+
61
+ existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
62
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
63
+ return nil, response.HandleGormError(err, "Internal Server Error")
64
+ }
65
+
66
+ if existingAttempt != nil {
67
+ return s.handleExistingAttempt(ctx, req, quiz, existingAttempt)
68
+ }
69
+
70
+ return s.handleNewAttempt(ctx, req, quiz)
71
+ }
72
+
73
+ func (s *quizService) handleExistingAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
74
+ now := time.Now()
75
+
76
+ if attempt.DueAt.Before(now) {
77
+ if attempt.FinishedAt == nil {
78
+ attempt.FinishedAt = &now
79
+ attempt.Score = s.calculateQuizScore(ctx, attempt)
80
+
81
+ if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
82
+ return nil, response.HandleGormError(err, "Internal Server Error")
83
+ }
84
+
85
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
86
+ if err != nil {
87
+ return nil, response.HandleGormError(err, "Internal Server Error")
88
+ }
89
+
90
+ if totalAttempts >= quiz.AttemptLimit {
91
+ if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
92
+ return nil, response.HandleGormError(err, "Internal Server Error")
93
+ }
94
+
95
+ // hapus juga semua attempt quiz by account id dan quiz id
96
+ if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
97
+ return nil, response.HandleGormError(err, "Internal Server Error")
98
+ }
99
+ }
100
+
101
+ return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
102
+ }
103
+
104
+ return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
105
+ }
106
+
107
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
108
+ if err != nil {
109
+ return nil, response.HandleGormError(err, "Internal Server Error")
110
+ }
111
+
112
+ questions = shuffleWithKey(questions, attempt.ID)
113
+
114
+ return &models.UserAttemptQuizResponse{
115
+ QuizAttempt: *attempt,
116
+ Questions: questions,
117
+ }, nil
118
+ }
119
+
120
+ func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
121
+ totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
122
+ if err != nil {
123
+ return nil, response.HandleGormError(err, "Internal Server Error")
124
+ }
125
+ if totalAttempts >= quiz.AttemptLimit {
126
+ return nil, models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
127
+ }
128
+
129
+ percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
130
+ if err != nil {
131
+ return nil, response.HandleGormError(err, "Internal Server Error")
132
+ }
133
+ if percentage < 100 {
134
+ return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
135
+ }
136
+
137
+ quizAttempt := models.QuizAttempt{
138
+ AccountID: req.AccountID,
139
+ QuizID: quiz.ID,
140
+ StartedAt: time.Now(),
141
+ DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
142
+ }
143
+
144
+ if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt); err != nil {
145
+ return nil, response.HandleGormError(err, "Internal Server Error")
146
+ }
147
+
148
+ questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
149
+ if err != nil {
150
+ return nil, response.HandleGormError(err, "Internal Server Error")
151
+ }
152
+
153
+ questions = shuffleWithKey(questions, quizAttempt.ID)
154
+
155
+ return &models.UserAttemptQuizResponse{
156
+ QuizAttempt: quizAttempt,
157
+ Questions: questions,
158
+ }, nil
159
+ }
160
+
161
+ func (s *quizService) calculateQuizScore(ctx context.Context, attempt *models.QuizAttempt) float64 {
162
+ // ambil total question dari quiz
163
+ totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
164
+ if err != nil {
165
+ return 0
166
+ }
167
+
168
+ if totalQuestion == 0 {
169
+ return 0
170
+ }
171
+
172
+ // ambil semua user answer yang is_correct nya true
173
+ correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
174
+ if err != nil {
175
+ return 0
176
+ }
177
+
178
+ // hitung score nya
179
+ score := float64(correctAnswer) / float64(totalQuestion) * 100
180
+
181
+ return score
182
+ }
183
+
184
+ // shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
185
+ // untuk memastikan hasil acak yang konsisten untuk key yang sama
186
+ func shuffleWithKey[T any](slice []T, key int64) []T {
187
+ // Buat salinan slice untuk menghindari modifikasi original
188
+ shuffled := make([]T, len(slice))
189
+ copy(shuffled, slice)
190
+
191
+ // Buat random source dengan seed dari key
192
+ r := rand.New(rand.NewSource(key))
193
+
194
+ // Lakukan shuffling
195
+ r.Shuffle(len(shuffled), func(i, j int) {
196
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
197
+ })
198
+
199
+ return shuffled
200
+ }
space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go CHANGED
@@ -349,15 +349,21 @@ func (c *academyController) UserGetAcademyMaterialBySlug(ctx *gin.Context) {
349
  }
350
 
351
  func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
352
- slug := ctx.Param("materialSlug")
 
 
 
 
 
 
 
353
 
354
  accountData := middleware.GetAccountData(ctx)
355
  accountID := int64(accountData.UserID)
356
 
357
- req := models.ToggleAcademyMaterialProgressRequest{
358
- AccountID: accountID,
359
- Slug: slug,
360
- }
361
 
362
  err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
363
  if err != nil {
 
349
  }
350
 
351
  func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
352
+ var req models.ToggleAcademyMaterialProgressRequest
353
+ if err := ctx.ShouldBindJSON(&req); err != nil {
354
+ response.HandleError(ctx, err)
355
+ return
356
+ }
357
+
358
+ academySlug := ctx.Param("slug")
359
+ materialSlug := ctx.Param("materialSlug")
360
 
361
  accountData := middleware.GetAccountData(ctx)
362
  accountID := int64(accountData.UserID)
363
 
364
+ req.AccountID = accountID
365
+ req.AcademySlug = academySlug
366
+ req.MaterialSlug = materialSlug
 
367
 
368
  err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
369
  if err != nil {
space/space/space/space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -80,6 +80,7 @@ type (
80
  Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
81
  Description string `gorm:"column:description" json:"description"`
82
  Order uint `gorm:"column:order" json:"order"`
 
83
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
84
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
85
  }
@@ -101,7 +102,8 @@ type (
101
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
102
  AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
103
  AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
104
- ReadAt *time.Time `gorm:"column:read_at" json:"read_at"`
 
105
  }
106
  )
107
 
 
80
  Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
81
  Description string `gorm:"column:description" json:"description"`
82
  Order uint `gorm:"column:order" json:"order"`
83
+ Image *string `gorm:"column:image" json:"image"`
84
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
85
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
86
  }
 
102
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
103
  AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
104
  AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
105
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
106
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
107
  }
108
  )
109
 
space/space/space/space/space/space/space/space/space/models/request_model.go CHANGED
@@ -56,8 +56,26 @@ type (
56
  }
57
 
58
  UserAcademyMaterialResponse struct {
59
- AcademyMaterial
60
- IsRead bool `gorm:"is_read" json:"is_read"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
  CreateAcademyMaterialRequest struct {
@@ -108,8 +126,10 @@ type (
108
  }
109
 
110
  ToggleAcademyMaterialProgressRequest struct {
111
- AccountID int64 `json:"account_id"`
112
- Slug string `json:"slug"`
 
 
113
  }
114
  )
115
 
@@ -131,6 +151,38 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
131
  }
132
  }
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  type (
135
  MultipleOptionsRequest struct {
136
  OptionName string `json:"option_name" validate:"required"`
 
56
  }
57
 
58
  UserAcademyMaterialResponse struct {
59
+ ID int64 `gorm:"column:id" json:"id"`
60
+ AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
61
+ Title string `gorm:"column:title" json:"title"`
62
+ Slug string `gorm:"column:slug" json:"slug"`
63
+ Order uint `gorm:"column:order" json:"order"`
64
+ CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
65
+ UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
66
+ IsRead bool `gorm:"is_read" json:"is_read"`
67
+ }
68
+
69
+ UserAcademyMaterialDetailResponse struct {
70
+ ID int64 `gorm:"column:id" json:"id"`
71
+ AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
72
+ Title string `gorm:"column:title" json:"title"`
73
+ Slug string `gorm:"column:slug" json:"slug"`
74
+ Content string `gorm:"column:content" json:"content"`
75
+ Order uint `gorm:"column:order" json:"order"`
76
+ CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
77
+ UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
78
+ IsRead bool `gorm:"is_read" json:"is_read"`
79
  }
80
 
81
  CreateAcademyMaterialRequest struct {
 
126
  }
127
 
128
  ToggleAcademyMaterialProgressRequest struct {
129
+ AccountID int64 `json:"-"`
130
+ MaterialSlug string `json:"-"`
131
+ AcademySlug string `json:"-"`
132
+ IsRead bool `json:"is_read"`
133
  }
134
  )
135
 
 
151
  }
152
  }
153
 
154
+ type (
155
+ ListQuizRequest struct {
156
+ AccountID int64 `json:"account_id"`
157
+ AcademyID int64 `json:"academy_id" validate:"required"`
158
+ }
159
+
160
+ ListQuizResponse struct {
161
+ Quiz []Quiz `json:"quiz"`
162
+ }
163
+
164
+ AttemptQuizRequest struct {
165
+ AccountID int64 `json:"account_id"`
166
+ QuizID int64 `json:"quiz_id" validate:"required"`
167
+ }
168
+
169
+ AttemptQuizResponse struct {
170
+ QuizAttempt QuizAttempt `json:"quiz_attempt"`
171
+ }
172
+
173
+ GetQuestionQuizRequest struct {
174
+ AccountID int64 `json:"account_id"`
175
+ QuizID int64 `json:"quiz_id" validate:"required"`
176
+ }
177
+
178
+ GetQuestionQuizResponse struct {
179
+ Question Question `json:"question"`
180
+ Answer []Answer `json:"answer_options"`
181
+ UserAnswer int `json:"current_user_answer"`
182
+ IsDoubt bool `json:"is_doubt"`
183
+ }
184
+ )
185
+
186
  type (
187
  MultipleOptionsRequest struct {
188
  OptionName string `json:"option_name" validate:"required"`
space/space/space/space/space/space/space/space/space/repositories/academy_repository.go CHANGED
@@ -28,7 +28,7 @@ type AcademyRepository interface {
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
- UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
@@ -245,7 +245,7 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
245
  Model(&models.Academy{}).
246
  Select(`academy.*,
247
  COUNT(DISTINCT am.id) AS total_material,
248
- COUNT(DISTINCT CASE WHEN amp.read_at IS NOT NULL THEN amp.academy_material_id END) AS total_read_material`).
249
  Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
250
  Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
251
  Group("academy.id")
@@ -294,14 +294,16 @@ func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *mo
294
  q := r.db.WithContext(ctx).
295
  Model(&models.AcademyMaterial{}).
296
  Select(`academy_materials.*,
297
- CASE
298
- WHEN amp.read_at IS NOT NULL THEN true
299
- ELSE false
300
- END AS is_read`).
 
 
301
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
302
  Joins(`LEFT JOIN academy_materials_progress amp ON
303
- amp.academy_material_id = academy_materials.id AND
304
- amp.account_id = ?`, req.AccountID).
305
  Where("academy.slug = ?", req.Slug)
306
 
307
  if req.Filter.HasKeyword() {
@@ -359,8 +361,8 @@ func (r *academyRepository) UserGetAcademyBySlug(ctx context.Context, slug strin
359
  return &academy, nil
360
  }
361
 
362
- func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialResponse, error) {
363
- var result models.UserAcademyMaterialResponse
364
 
365
  err := r.db.WithContext(ctx).
366
  Model(&models.AcademyMaterial{}).
@@ -369,22 +371,28 @@ func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, sl
369
  academy.title as academy_title,
370
  academy.slug as academy_slug,
371
  CASE
372
- WHEN amp.read_at IS NOT NULL THEN true
373
  ELSE false
374
- END AS is_read
 
 
375
  `).
376
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
377
  Joins(`LEFT JOIN academy_materials_progress amp ON
378
  amp.academy_material_id = academy_materials.id AND
379
  amp.account_id = ?`, accountID).
380
  Where("academy_materials.slug = ?", slug).
381
- First(&result).
382
  Error
383
 
384
  if err != nil {
385
  return nil, err
386
  }
387
 
 
 
 
 
388
  return &result, nil
389
  }
390
 
 
28
  UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
29
  UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
30
  UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
31
+ UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
32
  UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
33
  UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
34
  UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
 
245
  Model(&models.Academy{}).
246
  Select(`academy.*,
247
  COUNT(DISTINCT am.id) AS total_material,
248
+ COUNT(DISTINCT CASE WHEN amp.created_at IS NOT NULL THEN amp.academy_material_id END) AS total_read_material`).
249
  Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
250
  Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
251
  Group("academy.id")
 
294
  q := r.db.WithContext(ctx).
295
  Model(&models.AcademyMaterial{}).
296
  Select(`academy_materials.*,
297
+ amp.created_at AS read_created_at,
298
+ amp.updated_at AS read_updated_at,
299
+ CASE
300
+ WHEN amp.created_at IS NOT NULL THEN true
301
+ ELSE false
302
+ END AS is_read`).
303
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
304
  Joins(`LEFT JOIN academy_materials_progress amp ON
305
+ amp.academy_material_id = academy_materials.id AND
306
+ amp.account_id = ?`, req.AccountID).
307
  Where("academy.slug = ?", req.Slug)
308
 
309
  if req.Filter.HasKeyword() {
 
361
  return &academy, nil
362
  }
363
 
364
+ func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error) {
365
+ var result models.UserAcademyMaterialDetailResponse
366
 
367
  err := r.db.WithContext(ctx).
368
  Model(&models.AcademyMaterial{}).
 
371
  academy.title as academy_title,
372
  academy.slug as academy_slug,
373
  CASE
374
+ WHEN amp.created_at IS NOT NULL THEN true
375
  ELSE false
376
+ END AS is_read,
377
+ amp.created_at AS read_created_at,
378
+ amp.updated_at AS read_updated_at
379
  `).
380
  Joins("JOIN academy ON academy.id = academy_materials.academy_id").
381
  Joins(`LEFT JOIN academy_materials_progress amp ON
382
  amp.academy_material_id = academy_materials.id AND
383
  amp.account_id = ?`, accountID).
384
  Where("academy_materials.slug = ?", slug).
385
+ Scan(&result).
386
  Error
387
 
388
  if err != nil {
389
  return nil, err
390
  }
391
 
392
+ if result.ID == 0 {
393
+ return nil, gorm.ErrRecordNotFound
394
+ }
395
+
396
  return &result, nil
397
  }
398
 
space/space/space/space/space/space/space/space/space/repositories/quiz_repository.go CHANGED
@@ -1,9 +1,28 @@
1
  package repositories
2
 
3
  import (
 
 
4
  "api.qobiltu.id/models"
 
5
  )
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
8
  repo := Construct[models.Quiz, []models.Quiz](
9
  models.Quiz{AcademyID: academyId},
 
1
  package repositories
2
 
3
  import (
4
+ "context"
5
+
6
  "api.qobiltu.id/models"
7
+ "gorm.io/gorm"
8
  )
9
 
10
+ type QuizRepository interface {
11
+ UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
12
+ }
13
+
14
+ type quizRepository struct {
15
+ db *gorm.DB
16
+ }
17
+
18
+ func NewQuizRepository(db *gorm.DB) QuizRepository {
19
+ return &quizRepository{db: db}
20
+ }
21
+
22
+ func (r *quizRepository) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
23
+ panic("not implemented")
24
+ }
25
+
26
  func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
27
  repo := Construct[models.Quiz, []models.Quiz](
28
  models.Quiz{AcademyID: academyId},
space/space/space/space/space/space/space/space/space/router/quiz_route.go CHANGED
@@ -15,7 +15,7 @@ func QuizRoute(router *gin.Engine) {
15
  routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
- routerGroup.GET("result/", middleware.AuthUser, QuizController.Result)
19
  routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
  routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }
 
15
  routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
+ routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
19
  routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
20
  routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
21
  }