lifedebugger commited on
Commit
6b959b3
·
1 Parent(s): 6de5368

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. controller/cv/cv_controller.go +17 -2
  2. models/database_orm_model.go +37 -0
  3. models/field_counter.go +163 -0
  4. models/request_model.go +12 -0
  5. router/cv_route.go +2 -0
  6. services/cv_service.go +115 -0
  7. space/models/database_orm_model.go +47 -9
  8. space/repositories/quiz_repository.go +5 -2
  9. space/services/academy_quiz_service.go +1 -1
  10. space/space/models/request_model.go +3 -1
  11. space/space/pkg/validation/custom_rules.go +42 -0
  12. space/space/pkg/validation/validation.go +1 -0
  13. space/space/space/Makefile +9 -1
  14. space/space/space/config/config.go +3 -0
  15. space/space/space/controller/cv/cv_controller.go +20 -0
  16. space/space/space/docker-compose.yml +4 -7
  17. space/space/space/go.mod +1 -1
  18. space/space/space/main.go +8 -4
  19. space/space/space/models/exception_model.go +3 -1
  20. space/space/space/models/request_model.go +13 -1
  21. space/space/space/pkg/storage/local.go +67 -0
  22. space/space/space/pkg/storage/storage.go +71 -0
  23. space/space/space/response/api_response_v2.go +1 -1
  24. space/space/space/response/validation.go +1 -1
  25. space/space/space/router/cv_route.go +1 -0
  26. space/space/space/router/router.go +1 -0
  27. space/space/space/router/storage_route.go +24 -0
  28. space/space/space/services/cv_service.go +468 -385
  29. space/space/space/services/email_verification_service.go +78 -78
  30. space/space/space/services/forgot_password_service.go +1 -1
  31. space/space/space/space/.gitignore +1 -0
  32. space/space/space/space/pkg/validation/custom_rules.go +162 -0
  33. space/space/space/space/pkg/validation/validation.go +164 -0
  34. space/space/space/space/pkg/worker/distributor.go +23 -0
  35. space/space/space/space/pkg/worker/logger.go +37 -0
  36. space/space/space/space/pkg/worker/processor.go +66 -0
  37. space/space/space/space/pkg/worker/redis_distributor.go +16 -0
  38. space/space/space/space/pkg/worker/task_send_forgot_password_email.go +75 -0
  39. space/space/space/space/pkg/worker/task_send_verify_email.go +75 -0
  40. space/space/space/space/space/main.go +5 -0
  41. space/space/space/space/space/models/exception_model.go +18 -15
  42. space/space/space/space/space/response/api_response_v2.go +16 -1
  43. space/space/space/space/space/response/validation.go +14 -0
  44. space/space/space/space/space/services/cv_service.go +384 -343
  45. space/space/space/space/space/space/go.sum +2 -0
  46. space/space/space/space/space/space/models/database_orm_model.go +32 -31
  47. space/space/space/space/space/space/models/request_model.go +62 -59
  48. space/space/space/space/space/space/space/controller/quiz/list_quiz_controller.go +23 -0
  49. space/space/space/space/space/space/space/go.mod +4 -3
  50. space/space/space/space/space/space/space/router/quiz_route.go +1 -0
controller/cv/cv_controller.go CHANGED
@@ -1,13 +1,14 @@
1
  package cv_controller
2
 
3
  import (
 
 
 
4
  "api.qobiltu.id/middleware"
5
  "api.qobiltu.id/models"
6
  "api.qobiltu.id/response"
7
  "api.qobiltu.id/services"
8
  "github.com/gin-gonic/gin"
9
- "net/http"
10
- "strconv"
11
  )
12
 
13
  type CVController interface {
@@ -48,6 +49,7 @@ type CVController interface {
48
  DeleteAchievement(ctx *gin.Context)
49
 
50
  UploadProfileImage(ctx *gin.Context)
 
51
  }
52
 
53
  type cvController struct {
@@ -573,3 +575,16 @@ func (c *cvController) UploadProfileImage(ctx *gin.Context) {
573
 
574
  response.HandleSuccess(ctx, http.StatusOK, "Profile image uploaded successfully", res, nil)
575
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package cv_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 CVController interface {
 
49
  DeleteAchievement(ctx *gin.Context)
50
 
51
  UploadProfileImage(ctx *gin.Context)
52
+ GetProgress(ctx *gin.Context)
53
  }
54
 
55
  type cvController struct {
 
575
 
576
  response.HandleSuccess(ctx, http.StatusOK, "Profile image uploaded successfully", res, nil)
577
  }
578
+
579
+ func (c *cvController) GetProgress(ctx *gin.Context) {
580
+ accountData := middleware.GetAccountData(ctx)
581
+ accountID := int64(accountData.UserID)
582
+
583
+ res, err := c.cvService.GetProgress(ctx, accountID)
584
+ if err != nil {
585
+ response.HandleError(ctx, err)
586
+ return
587
+ }
588
+
589
+ response.HandleSuccess(ctx, http.StatusOK, "Progress retrieved successfully", res, nil)
590
+ }
models/database_orm_model.go CHANGED
@@ -34,6 +34,7 @@ type AccountDetails struct {
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
37
  }
38
 
39
  type EmailVerification struct {
@@ -210,6 +211,7 @@ type (
210
  MonthlyExpenses *string `gorm:"column:monthly_expenses" json:"monthly_expenses"` // pengeluaran per bulan
211
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
212
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
213
  }
214
 
215
  FamilyMemberCV struct {
@@ -224,6 +226,7 @@ type (
224
  Age *int `gorm:"column:age" json:"age"` // Usia
225
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
226
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
227
  }
228
 
229
  PhysicalAndHealthCV struct {
@@ -240,6 +243,7 @@ type (
240
  PhysicalTraits *string `gorm:"column:physical_traits" json:"physical_traits"` // Ciri khas fisik
241
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // Waktu data dibuat
242
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // Waktu data terakhir diperbarui
 
243
  }
244
 
245
  WorshipAndReligiousUnderstandingCV struct {
@@ -263,6 +267,7 @@ type (
263
  FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
264
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
265
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
266
  }
267
 
268
  EducationCV struct {
@@ -301,6 +306,38 @@ type (
301
  }
302
  )
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  // Gorm table name settings
305
  func (Account) TableName() string { return "account" }
306
  func (AccountDetails) TableName() string { return "account_details" }
 
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
37
+ FieldCounter
38
  }
39
 
40
  type EmailVerification struct {
 
211
  MonthlyExpenses *string `gorm:"column:monthly_expenses" json:"monthly_expenses"` // pengeluaran per bulan
212
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
213
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
214
+ FieldCounter
215
  }
216
 
217
  FamilyMemberCV struct {
 
226
  Age *int `gorm:"column:age" json:"age"` // Usia
227
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
228
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
229
+ FieldCounter
230
  }
231
 
232
  PhysicalAndHealthCV struct {
 
243
  PhysicalTraits *string `gorm:"column:physical_traits" json:"physical_traits"` // Ciri khas fisik
244
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // Waktu data dibuat
245
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // Waktu data terakhir diperbarui
246
+ FieldCounter
247
  }
248
 
249
  WorshipAndReligiousUnderstandingCV struct {
 
267
  FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
268
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
269
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
270
+ FieldCounter
271
  }
272
 
273
  EducationCV struct {
 
306
  }
307
  )
308
 
309
+ func (a AccountDetails) TotalFields() int {
310
+ return a.FieldCounter.TotalFields(a)
311
+ }
312
+
313
+ func (a AccountDetails) GetFilledFields() []string {
314
+ return a.FieldCounter.GetFilledFields(a)
315
+ }
316
+
317
+ func (p PersonalityAndPreferenceCV) TotalFields() int {
318
+ return p.FieldCounter.TotalFields(p)
319
+ }
320
+
321
+ func (p PersonalityAndPreferenceCV) GetFilledFields() []string {
322
+ return p.FieldCounter.GetFilledFields(p)
323
+ }
324
+
325
+ func (p PhysicalAndHealthCV) TotalFields() int {
326
+ return p.FieldCounter.TotalFields(p)
327
+ }
328
+
329
+ func (p PhysicalAndHealthCV) GetFilledFields() []string {
330
+ return p.FieldCounter.GetFilledFields(p)
331
+ }
332
+
333
+ func (w WorshipAndReligiousUnderstandingCV) TotalFields() int {
334
+ return w.FieldCounter.TotalFields(w)
335
+ }
336
+
337
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFields() []string {
338
+ return w.FieldCounter.GetFilledFields(w)
339
+ }
340
+
341
  // Gorm table name settings
342
  func (Account) TableName() string { return "account" }
343
  func (AccountDetails) TableName() string { return "account_details" }
models/field_counter.go ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "reflect"
5
+ "time"
6
+ )
7
+
8
+ // CounterGetter adalah interface yang menyediakan method untuk menghitung dan mendapatkan field
9
+ type CounterGetter interface {
10
+ TotalFields() int
11
+ GetFilledFields() []string
12
+ }
13
+
14
+ // FieldCounter menyediakan fungsionalitas generik penghitungan dan pengecekan field
15
+ // untuk diembed ke dalam struct lain
16
+ type FieldCounter struct{}
17
+
18
+ // shouldSkipField menentukan apakah field harus dilewati dalam penghitungan
19
+ func shouldSkipField(field reflect.StructField) bool {
20
+ // Skip field dengan nama FieldCounter (embedded type)
21
+ if field.Name == "FieldCounter" {
22
+ return true
23
+ }
24
+
25
+ // Skip field yang berasal dari GORM atau standard fields
26
+ // seperti ID, timestamps, dan foreign keys
27
+ if field.Name == "ID" || field.Name == "CreatedAt" || field.Name == "UpdatedAt" || field.Name == "DeletedAt" ||
28
+ field.Name == "AccountID" || field.Name == "Account" {
29
+ return true
30
+ }
31
+
32
+ // Periksa tag gorm untuk auto fields
33
+ gormTag := field.Tag.Get("gorm")
34
+ if len(gormTag) > 0 {
35
+ // Skip kolom yang auto-increment atau auto-timestamp
36
+ if gormTag == "primaryKey" || gormTag == "autoIncrement" ||
37
+ gormTag == "autoCreateTime" || gormTag == "autoUpdateTime" {
38
+ return true
39
+ }
40
+ }
41
+
42
+ return false
43
+ }
44
+
45
+ // TotalFields menghitung jumlah total field dalam struct
46
+ // dengan mengecualikan field internal, metadata, dan embedded FieldCounter
47
+ func (fc FieldCounter) TotalFields(s any) int {
48
+ t := reflect.TypeOf(s)
49
+ count := 0
50
+
51
+ // Jika s adalah pointer, dapatkan elemen yang ditunjuknya
52
+ if t.Kind() == reflect.Ptr {
53
+ t = t.Elem()
54
+ }
55
+
56
+ // Pastikan kita berurusan dengan struct
57
+ if t.Kind() != reflect.Struct {
58
+ return 0
59
+ }
60
+
61
+ // Hitung field yang sesuai dengan kriteria
62
+ for i := 0; i < t.NumField(); i++ {
63
+ field := t.Field(i)
64
+
65
+ // Skip field yang memenuhi kriteria yang ditentukan
66
+ if shouldSkipField(field) {
67
+ continue
68
+ }
69
+
70
+ count++
71
+ }
72
+
73
+ return count
74
+ }
75
+
76
+ // GetFilledFields mengembalikan slice berisi nama field yang telah diisi dengan nilai
77
+ // (tidak nil, string tidak kosong, int/float tidak 0, dll)
78
+ func (fc FieldCounter) GetFilledFields(s any) []string {
79
+ var filledFields []string
80
+ v := reflect.ValueOf(s)
81
+
82
+ // Jika s adalah pointer, dapatkan elemen yang ditunjuknya
83
+ if v.Kind() == reflect.Ptr {
84
+ if v.IsNil() {
85
+ return filledFields
86
+ }
87
+ v = v.Elem()
88
+ }
89
+
90
+ // Pastikan kita berurusan dengan struct
91
+ if v.Kind() != reflect.Struct {
92
+ return filledFields
93
+ }
94
+
95
+ t := v.Type()
96
+
97
+ // Loop melalui semua field
98
+ for i := 0; i < v.NumField(); i++ {
99
+ field := v.Field(i)
100
+ fieldType := t.Field(i)
101
+ fieldName := fieldType.Name
102
+
103
+ // Skip field yang memenuhi kriteria yang ditentukan
104
+ if shouldSkipField(fieldType) {
105
+ continue
106
+ }
107
+
108
+ // Cek apakah field diisi berdasarkan tipenya
109
+ if isFieldFilled(field) {
110
+ filledFields = append(filledFields, fieldName)
111
+ }
112
+ }
113
+
114
+ return filledFields
115
+ }
116
+
117
+ // isFieldFilled memeriksa apakah field memiliki nilai non-zero
118
+ func isFieldFilled(field reflect.Value) bool {
119
+ // Jika field tidak dapat di-address atau diakses, anggap kosong
120
+ if !field.IsValid() || !field.CanInterface() {
121
+ return false
122
+ }
123
+
124
+ // Handle untuk pointer
125
+ if field.Kind() == reflect.Ptr {
126
+ if field.IsNil() {
127
+ return false
128
+ }
129
+ // Untuk pointer, periksa nilai yang ditunjuk
130
+ return isFieldFilled(field.Elem())
131
+ }
132
+
133
+ // Cek berdasarkan tipe data
134
+ switch field.Kind() {
135
+ case reflect.String:
136
+ return field.String() != ""
137
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
138
+ return field.Int() != 0
139
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
140
+ return field.Uint() != 0
141
+ case reflect.Float32, reflect.Float64:
142
+ return field.Float() != 0
143
+ case reflect.Bool:
144
+ return field.Bool()
145
+ case reflect.Slice, reflect.Map, reflect.Array:
146
+ return !field.IsNil() && field.Len() > 0
147
+ case reflect.Struct:
148
+ // Perlakuan khusus untuk time.Time
149
+ if field.Type() == reflect.TypeOf(time.Time{}) {
150
+ // Anggap time.Time kosong jika zero value atau tahun sangat awal
151
+ timeVal := field.Interface().(time.Time)
152
+ return !timeVal.IsZero() && timeVal.Year() > 1
153
+ }
154
+
155
+ // Untuk struct lain, bandingkan dengan zero value
156
+ return !reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface())
157
+ case reflect.Interface:
158
+ return !field.IsNil()
159
+ default:
160
+ // Untuk tipe lain, gunakan perbandingan dengan zero value
161
+ return !field.IsZero()
162
+ }
163
+ }
models/request_model.go CHANGED
@@ -161,4 +161,16 @@ type (
161
  UploadProfileImageResponse struct {
162
  URL string `json:"url"`
163
  }
 
 
 
 
 
 
 
 
 
 
 
 
164
  )
 
161
  UploadProfileImageResponse struct {
162
  URL string `json:"url"`
163
  }
164
+
165
+ ProgressResponse struct {
166
+ AccountDetailsProgress float64 `json:"account_details_progress"`
167
+ PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
168
+ FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
169
+ PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
170
+ WorshipAndReligiousUnderstandingCVProgress float64 `json:"worship_and_religious_understanding_cv_progress"`
171
+ EducationCVProgress float64 `json:"education_cv_progress"`
172
+ JobCVProgress float64 `json:"job_cv_progress"`
173
+ AchievementCVProgress float64 `json:"achievement_cv_progress"`
174
+ TotalProgress float64 `json:"total_progress"`
175
+ }
176
  )
router/cv_route.go CHANGED
@@ -41,5 +41,7 @@ func (s *Server) CVRoute() {
41
  routerGroup.GET("/achievements/:id", s.cvController.GetAchievement)
42
  routerGroup.PUT("/achievements/:id", s.cvController.UpdateAchievement)
43
  routerGroup.DELETE("/achievements/:id", s.cvController.DeleteAchievement)
 
 
44
  }
45
  }
 
41
  routerGroup.GET("/achievements/:id", s.cvController.GetAchievement)
42
  routerGroup.PUT("/achievements/:id", s.cvController.UpdateAchievement)
43
  routerGroup.DELETE("/achievements/:id", s.cvController.DeleteAchievement)
44
+
45
+ routerGroup.GET("/progress", s.cvController.GetProgress)
46
  }
47
  }
services/cv_service.go CHANGED
@@ -3,6 +3,8 @@ package services
3
  import (
4
  "context"
5
  "errors"
 
 
6
  "strconv"
7
 
8
  "api.qobiltu.id/models"
@@ -51,6 +53,7 @@ type CVService interface {
51
  DeleteAchievement(ctx context.Context, id int64) error
52
 
53
  UploadProfileImage(ctx context.Context, req *models.UploadProfileImageRequest) (*models.UploadProfileImageResponse, error)
 
54
  }
55
 
56
  type cvService struct {
@@ -566,3 +569,115 @@ func (s *cvService) UploadProfileImage(ctx context.Context, req *models.UploadPr
566
  URL: url,
567
  }, nil
568
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import (
4
  "context"
5
  "errors"
6
+ "fmt"
7
+ "math"
8
  "strconv"
9
 
10
  "api.qobiltu.id/models"
 
53
  DeleteAchievement(ctx context.Context, id int64) error
54
 
55
  UploadProfileImage(ctx context.Context, req *models.UploadProfileImageRequest) (*models.UploadProfileImageResponse, error)
56
+ GetProgress(ctx context.Context, accountID int64) (*models.ProgressResponse, error)
57
  }
58
 
59
  type cvService struct {
 
569
  URL: url,
570
  }, nil
571
  }
572
+
573
+ func (s *cvService) GetProgress(ctx context.Context, accountID int64) (*models.ProgressResponse, error) {
574
+ accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, accountID)
575
+ if err != nil {
576
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
577
+ return nil, response.HandleGormError(err, "Internal Server Error")
578
+ }
579
+ accountDetails = &models.AccountDetails{}
580
+ }
581
+
582
+ personalityAndPreferenceCV, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, accountID)
583
+ if err != nil {
584
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
585
+ return nil, response.HandleGormError(err, "Internal Server Error")
586
+ }
587
+ personalityAndPreferenceCV = &models.PersonalityAndPreferenceCV{}
588
+ }
589
+
590
+ physicalAndHealthCV, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, accountID)
591
+ if err != nil {
592
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
593
+ return nil, response.HandleGormError(err, "Internal Server Error")
594
+ }
595
+ physicalAndHealthCV = &models.PhysicalAndHealthCV{}
596
+ }
597
+
598
+ worshipAndReligiousUnderstandingCV, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, accountID)
599
+ if err != nil {
600
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
601
+ return nil, response.HandleGormError(err, "Internal Server Error")
602
+ }
603
+ worshipAndReligiousUnderstandingCV = &models.WorshipAndReligiousUnderstandingCV{}
604
+ }
605
+
606
+ educationCV, err := s.cvRepository.ListEducation(ctx, accountID)
607
+ if err != nil {
608
+ return nil, response.HandleGormError(err, "Internal Server Error")
609
+ }
610
+
611
+ familyMemberCV, err := s.cvRepository.ListFamilyMember(ctx, accountID)
612
+ if err != nil {
613
+ return nil, response.HandleGormError(err, "Internal Server Error")
614
+ }
615
+
616
+ jobCV, err := s.cvRepository.ListJob(ctx, accountID)
617
+ if err != nil {
618
+ return nil, response.HandleGormError(err, "Internal Server Error")
619
+ }
620
+
621
+ achievementCV, err := s.cvRepository.ListAchievement(ctx, accountID)
622
+ if err != nil {
623
+ return nil, response.HandleGormError(err, "Internal Server Error")
624
+ }
625
+
626
+ progressDetails := calculateProgress(
627
+ accountDetails,
628
+ personalityAndPreferenceCV,
629
+ physicalAndHealthCV,
630
+ worshipAndReligiousUnderstandingCV,
631
+ len(educationCV),
632
+ len(familyMemberCV),
633
+ len(jobCV),
634
+ len(achievementCV),
635
+ )
636
+
637
+ return progressDetails, nil
638
+ }
639
+
640
+ func calculateProgress(
641
+ accountDetails *models.AccountDetails,
642
+ personalityAndPreferenceCV *models.PersonalityAndPreferenceCV,
643
+ physicalAndHealthCV *models.PhysicalAndHealthCV,
644
+ worshipAndReligiousUnderstandingCV *models.WorshipAndReligiousUnderstandingCV,
645
+ educationCV int,
646
+ familyMemberCV int,
647
+ jobCV int,
648
+ achievementCV int,
649
+ ) *models.ProgressResponse {
650
+
651
+ exists := func(data int) float64 {
652
+ if data > 0 {
653
+ return 100
654
+ }
655
+ return 0
656
+ }
657
+
658
+ fmt.Println("accountDetails", accountDetails.GetFilledFields())
659
+ fmt.Println("accountDetailsTotal", accountDetails.TotalFields())
660
+
661
+ accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
662
+ personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
663
+ physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
664
+ worshipAndReligiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFields())) / float64(worshipAndReligiousUnderstandingCV.TotalFields()) * 100
665
+ edicationCVPercentage := exists(educationCV)
666
+ familyMemberCVPercentage := exists(familyMemberCV)
667
+ jobCVPercentage := exists(jobCV)
668
+ achievementCVPercentage := exists(achievementCV)
669
+
670
+ overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipAndReligiousUnderstandingCVPercentage + edicationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 8
671
+
672
+ return &models.ProgressResponse{
673
+ AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
674
+ PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
675
+ FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
676
+ PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
677
+ WorshipAndReligiousUnderstandingCVProgress: math.Round(worshipAndReligiousUnderstandingCVPercentage*100) / 100,
678
+ EducationCVProgress: math.Round(edicationCVPercentage*100) / 100,
679
+ JobCVProgress: jobCVPercentage,
680
+ AchievementCVProgress: achievementCVPercentage,
681
+ TotalProgress: math.Round(overallProgress*100) / 100,
682
+ }
683
+ }
space/models/database_orm_model.go CHANGED
@@ -34,6 +34,7 @@ type AccountDetails struct {
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
37
  }
38
 
39
  type EmailVerification struct {
@@ -154,15 +155,16 @@ type Question struct {
154
  CorrectAnswer uint `json:"-"`
155
  }
156
  type Quiz struct {
157
- ID uint `gorm:"primaryKey" json:"id"`
158
- AcademyID uint `json:"academy_id"`
159
- Slug string `json:"slug" gorm:"uniqueIndex" `
160
- Title string `json:"title"`
161
- Description string `json:"description"`
162
- AttemptLimit int `json:"attempt_limit"`
163
- TimeLimit int `json:"time_limit"`
164
- MinScore int `json:"min_score"`
165
- CreatedAt time.Time `json:"created_at"`
 
166
  }
167
 
168
  type QuizAttempt struct {
@@ -209,6 +211,7 @@ type (
209
  MonthlyExpenses *string `gorm:"column:monthly_expenses" json:"monthly_expenses"` // pengeluaran per bulan
210
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
211
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
212
  }
213
 
214
  FamilyMemberCV struct {
@@ -223,6 +226,7 @@ type (
223
  Age *int `gorm:"column:age" json:"age"` // Usia
224
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
225
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
226
  }
227
 
228
  PhysicalAndHealthCV struct {
@@ -239,6 +243,7 @@ type (
239
  PhysicalTraits *string `gorm:"column:physical_traits" json:"physical_traits"` // Ciri khas fisik
240
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // Waktu data dibuat
241
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // Waktu data terakhir diperbarui
 
242
  }
243
 
244
  WorshipAndReligiousUnderstandingCV struct {
@@ -262,6 +267,7 @@ type (
262
  FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
263
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
264
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
 
265
  }
266
 
267
  EducationCV struct {
@@ -300,6 +306,38 @@ type (
300
  }
301
  )
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  // Gorm table name settings
304
  func (Account) TableName() string { return "account" }
305
  func (AccountDetails) TableName() string { return "account_details" }
 
34
  PhoneNumber *string `gorm:"column:phone_number" json:"phone_number"`
35
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
36
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
37
+ FieldCounter
38
  }
39
 
40
  type EmailVerification struct {
 
155
  CorrectAnswer uint `json:"-"`
156
  }
157
  type Quiz struct {
158
+ ID uint `gorm:"primaryKey" json:"id"`
159
+ AcademyID uint `json:"academy_id"`
160
+ Slug string `json:"slug" gorm:"uniqueIndex" `
161
+ Title string `json:"title"`
162
+ Description string `json:"description"`
163
+ TotalQuestions string `json:"total_questions"`
164
+ AttemptLimit int `json:"attempt_limit"`
165
+ TimeLimit int `json:"time_limit"`
166
+ MinScore int `json:"min_score"`
167
+ CreatedAt time.Time `json:"created_at"`
168
  }
169
 
170
  type QuizAttempt struct {
 
211
  MonthlyExpenses *string `gorm:"column:monthly_expenses" json:"monthly_expenses"` // pengeluaran per bulan
212
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
213
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
214
+ FieldCounter
215
  }
216
 
217
  FamilyMemberCV struct {
 
226
  Age *int `gorm:"column:age" json:"age"` // Usia
227
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
228
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
229
+ FieldCounter
230
  }
231
 
232
  PhysicalAndHealthCV struct {
 
243
  PhysicalTraits *string `gorm:"column:physical_traits" json:"physical_traits"` // Ciri khas fisik
244
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // Waktu data dibuat
245
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // Waktu data terakhir diperbarui
246
+ FieldCounter
247
  }
248
 
249
  WorshipAndReligiousUnderstandingCV struct {
 
267
  FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
268
  CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
269
  UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
270
+ FieldCounter
271
  }
272
 
273
  EducationCV struct {
 
306
  }
307
  )
308
 
309
+ func (a AccountDetails) TotalFields() int {
310
+ return a.FieldCounter.TotalFields(a)
311
+ }
312
+
313
+ func (a AccountDetails) GetFilledFields() []string {
314
+ return a.FieldCounter.GetFilledFields(a)
315
+ }
316
+
317
+ func (p PersonalityAndPreferenceCV) TotalFields() int {
318
+ return p.FieldCounter.TotalFields(p)
319
+ }
320
+
321
+ func (p PersonalityAndPreferenceCV) GetFilledFields() []string {
322
+ return p.FieldCounter.GetFilledFields(p)
323
+ }
324
+
325
+ func (p PhysicalAndHealthCV) TotalFields() int {
326
+ return p.FieldCounter.TotalFields(p)
327
+ }
328
+
329
+ func (p PhysicalAndHealthCV) GetFilledFields() []string {
330
+ return p.FieldCounter.GetFilledFields(p)
331
+ }
332
+
333
+ func (w WorshipAndReligiousUnderstandingCV) TotalFields() int {
334
+ return w.FieldCounter.TotalFields(w)
335
+ }
336
+
337
+ func (w WorshipAndReligiousUnderstandingCV) GetFilledFields() []string {
338
+ return w.FieldCounter.GetFilledFields(w)
339
+ }
340
+
341
  // Gorm table name settings
342
  func (Account) TableName() string { return "account" }
343
  func (AccountDetails) TableName() string { return "account_details" }
space/repositories/quiz_repository.go CHANGED
@@ -49,9 +49,12 @@ func GetUserLastAttempt(userId uint, quizId uint) Repository[models.QuizAttempt,
49
  return *repo
50
  }
51
 
52
- func GetAttemptById(attemptId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
53
  repo := Construct[models.QuizAttempt, models.QuizAttempt](
54
- models.QuizAttempt{ID: attemptId},
 
 
 
55
  )
56
  repo.Transactions(
57
  WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
 
49
  return *repo
50
  }
51
 
52
+ func GetAttemptByIdandUserId(attemptId uint, userId uint) Repository[models.QuizAttempt, models.QuizAttempt] {
53
  repo := Construct[models.QuizAttempt, models.QuizAttempt](
54
+ models.QuizAttempt{
55
+ ID: attemptId,
56
+ AccountID: userId,
57
+ },
58
  )
59
  repo.Transactions(
60
  WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
space/services/academy_quiz_service.go CHANGED
@@ -131,7 +131,7 @@ func (s *AttemptQuizService) Create(userID uint) {
131
  }
132
 
133
  func (s *SubmitQuizService) Create(userID uint) {
134
- quizAttemptRepo := repositories.GetAttemptById(s.Constructor.ID)
135
  if quizAttemptRepo.NoRecord {
136
  s.Exception.DataNotFound = true
137
  s.Exception.Message = "There is no quiz attempt with given user!"
 
131
  }
132
 
133
  func (s *SubmitQuizService) Create(userID uint) {
134
+ quizAttemptRepo := repositories.GetAttemptByIdandUserId(s.Constructor.ID, userID)
135
  if quizAttemptRepo.NoRecord {
136
  s.Exception.DataNotFound = true
137
  s.Exception.Message = "There is no quiz attempt with given user!"
space/space/models/request_model.go CHANGED
@@ -1,9 +1,10 @@
1
  package models
2
 
3
  import (
4
- "github.com/lib/pq"
5
  "mime/multipart"
6
  "time"
 
 
7
  )
8
 
9
  type LoginRequest struct {
@@ -107,6 +108,7 @@ type (
107
  MaritalStatus *string `json:"marital_status" validate:"marital_status"`
108
  LastEducation *string `json:"last_education" validate:"last_education"`
109
  LastJob *string `json:"last_job"`
 
110
  }
111
 
112
  WorshipAndReligiousUnderstandingRequest struct {
 
1
  package models
2
 
3
  import (
 
4
  "mime/multipart"
5
  "time"
6
+
7
+ "github.com/lib/pq"
8
  )
9
 
10
  type LoginRequest struct {
 
108
  MaritalStatus *string `json:"marital_status" validate:"marital_status"`
109
  LastEducation *string `json:"last_education" validate:"last_education"`
110
  LastJob *string `json:"last_job"`
111
+ PhoneNumber *string `json:"phone_number" validate:"phone_number"`
112
  }
113
 
114
  WorshipAndReligiousUnderstandingRequest struct {
space/space/pkg/validation/custom_rules.go CHANGED
@@ -1,6 +1,7 @@
1
  package validation
2
 
3
  import (
 
4
  "strings"
5
  "sync"
6
 
@@ -150,6 +151,11 @@ func (v *Validator) RegisterAllCustomRules(validate *v10.Validate) error {
150
  return err
151
  }
152
 
 
 
 
 
 
153
  return nil
154
  }
155
 
@@ -160,3 +166,39 @@ func (v *Validator) PasswordRule(fl v10.FieldLevel) bool {
160
  strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") &&
161
  strings.ContainsAny(password, "0123456789")
162
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package validation
2
 
3
  import (
4
+ "regexp"
5
  "strings"
6
  "sync"
7
 
 
151
  return err
152
  }
153
 
154
+ err = validate.RegisterValidation("phone_number", v.PhoneNumberRule)
155
+ if err != nil {
156
+ return err
157
+ }
158
+
159
  return nil
160
  }
161
 
 
166
  strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") &&
167
  strings.ContainsAny(password, "0123456789")
168
  }
169
+
170
+ func (v *Validator) PhoneNumberRule(fl v10.FieldLevel) bool {
171
+ phone := SanitizePhoneNumber(fl.Field().String())
172
+ return strings.HasPrefix(phone, "+62")
173
+ }
174
+
175
+ func SanitizePhoneNumber(input string) string {
176
+ // Hilangkan semua spasi dan strip
177
+ input = strings.ReplaceAll(input, " ", "")
178
+ input = strings.ReplaceAll(input, "-", "")
179
+ input = strings.ReplaceAll(input, "(", "")
180
+ input = strings.ReplaceAll(input, ")", "")
181
+
182
+ // Hilangkan semua karakter non-digit kecuali +
183
+ re := regexp.MustCompile(`[^0-9\+]`)
184
+ input = re.ReplaceAllString(input, "")
185
+
186
+ // Handle nomor diawali 0 (contoh: 0812...) menjadi +62812...
187
+ if strings.HasPrefix(input, "0") {
188
+ input = "+62" + input[1:]
189
+ }
190
+
191
+ // Handle jika diawali dengan 62 tanpa + (contoh: 62812...)
192
+ if strings.HasPrefix(input, "62") && !strings.HasPrefix(input, "+62") {
193
+ input = "+" + input
194
+ }
195
+
196
+ // Handle jika tidak ada awalan +62 sama sekali (contoh: 8123456789)
197
+ if !strings.HasPrefix(input, "+62") {
198
+ if strings.HasPrefix(input, "8") {
199
+ input = "+62" + input
200
+ }
201
+ }
202
+
203
+ return input
204
+ }
space/space/pkg/validation/validation.go CHANGED
@@ -79,6 +79,7 @@ func setupValidations(validate *v10.Validate) error {
79
  if err := rules.RegisterAllCustomRules(validate); err != nil {
80
  return err
81
  }
 
82
  return nil
83
  }
84
 
 
79
  if err := rules.RegisterAllCustomRules(validate); err != nil {
80
  return err
81
  }
82
+
83
  return nil
84
  }
85
 
space/space/space/Makefile CHANGED
@@ -1,7 +1,15 @@
1
  up-dev:
2
  docker compose -f docker-compose.dev.yml up -d
3
 
 
 
 
 
 
 
 
 
4
  run-dev:
5
  go run main.go
6
 
7
- .PHONY : up-dev run-dev
 
1
  up-dev:
2
  docker compose -f docker-compose.dev.yml up -d
3
 
4
+ down-dev:
5
+ docker compose -f docker-compose.dev.yml down
6
+
7
+ up:
8
+ docker compose up -d --build
9
+ down:
10
+ docker compose down
11
+
12
  run-dev:
13
  go run main.go
14
 
15
+ .PHONY : up-dev run-dev down-dev up down
space/space/space/config/config.go CHANGED
@@ -29,6 +29,8 @@ var REDIS_MIN_IDLE_CONNS int
29
  var REDIS_POOL_SIZE int
30
  var REDIS_POOL_TIMEOUT time.Duration
31
 
 
 
32
  func init() {
33
  godotenv.Load()
34
  ENV = os.Getenv("ENV")
@@ -49,6 +51,7 @@ func init() {
49
  REDIS_POOL_SIZE = getValue(os.Getenv("REDIS_POOL_SIZE"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
50
  REDIS_MIN_IDLE_CONNS = getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
51
  REDIS_POOL_TIMEOUT = getValue(os.Getenv("REDIS_POOL_TIMEOUT"), time.Second*30, func(s string) (time.Duration, error) { return time.ParseDuration(s) })
 
52
  }
53
 
54
  func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
 
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")
 
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 {
space/space/space/controller/cv/cv_controller.go CHANGED
@@ -46,6 +46,8 @@ type CVController interface {
46
  ListAchievement(ctx *gin.Context)
47
  GetAchievement(ctx *gin.Context)
48
  DeleteAchievement(ctx *gin.Context)
 
 
49
  }
50
 
51
  type cvController struct {
@@ -553,3 +555,21 @@ func (c *cvController) DeleteAchievement(ctx *gin.Context) {
553
 
554
  response.HandleSuccess(ctx, http.StatusOK, "Achievement deleted successfully", nil, nil)
555
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  ListAchievement(ctx *gin.Context)
47
  GetAchievement(ctx *gin.Context)
48
  DeleteAchievement(ctx *gin.Context)
49
+
50
+ UploadProfileImage(ctx *gin.Context)
51
  }
52
 
53
  type cvController struct {
 
555
 
556
  response.HandleSuccess(ctx, http.StatusOK, "Achievement deleted successfully", nil, nil)
557
  }
558
+
559
+ func (c *cvController) UploadProfileImage(ctx *gin.Context) {
560
+ accountData := middleware.GetAccountData(ctx)
561
+
562
+ avatarFile, _ := ctx.FormFile("avatar")
563
+ req := models.UploadProfileImageRequest{
564
+ AccountID: int64(accountData.UserID),
565
+ File: avatarFile,
566
+ }
567
+
568
+ res, err := c.cvService.UploadProfileImage(ctx, &req)
569
+ if err != nil {
570
+ response.HandleError(ctx, err)
571
+ return
572
+ }
573
+
574
+ response.HandleSuccess(ctx, http.StatusOK, "Profile image uploaded successfully", res, nil)
575
+ }
space/space/space/docker-compose.yml CHANGED
@@ -6,14 +6,15 @@ services:
6
  build: .
7
  depends_on:
8
  - db
 
9
  env_file: .env
10
  ports:
11
  - "8080:8080"
 
 
 
12
  networks:
13
  - qobiltu-network
14
- # volumes:
15
- # - ./logs:/app/logs
16
- # - /home/qobiltu/api-qobiltu:/app
17
  restart: unless-stopped
18
 
19
  db:
@@ -45,10 +46,6 @@ services:
45
  - qobiltu-network
46
  restart: always
47
 
48
- volumes:
49
- db-data:
50
- redis-data:
51
-
52
  networks:
53
  qobiltu-network:
54
  driver: bridge
 
6
  build: .
7
  depends_on:
8
  - db
9
+ - redis
10
  env_file: .env
11
  ports:
12
  - "8080:8080"
13
+ volumes:
14
+ - ./logs:/app/logs
15
+ - ./uploads:/app/uploads
16
  networks:
17
  - qobiltu-network
 
 
 
18
  restart: unless-stopped
19
 
20
  db:
 
46
  - qobiltu-network
47
  restart: always
48
 
 
 
 
 
49
  networks:
50
  qobiltu-network:
51
  driver: bridge
space/space/space/go.mod CHANGED
@@ -8,6 +8,7 @@ require (
8
  github.com/go-playground/universal-translator v0.18.1
9
  github.com/go-playground/validator/v10 v10.25.0
10
  github.com/golang-jwt/jwt/v5 v5.2.1
 
11
  github.com/gosimple/slug v1.15.0
12
  github.com/hibiken/asynq v0.25.1
13
  github.com/joho/godotenv v1.5.1
@@ -37,7 +38,6 @@ require (
37
  github.com/go-logr/stdr v1.2.2 // indirect
38
  github.com/goccy/go-json v0.10.5 // indirect
39
  github.com/google/s2a-go v0.1.9 // indirect
40
- github.com/google/uuid v1.6.0 // indirect
41
  github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
42
  github.com/googleapis/gax-go/v2 v2.14.1 // indirect
43
  github.com/gosimple/unidecode v1.0.1 // indirect
 
8
  github.com/go-playground/universal-translator v0.18.1
9
  github.com/go-playground/validator/v10 v10.25.0
10
  github.com/golang-jwt/jwt/v5 v5.2.1
11
+ github.com/google/uuid v1.6.0
12
  github.com/gosimple/slug v1.15.0
13
  github.com/hibiken/asynq v0.25.1
14
  github.com/joho/godotenv v1.5.1
 
38
  github.com/go-logr/stdr v1.2.2 // indirect
39
  github.com/goccy/go-json v0.10.5 // indirect
40
  github.com/google/s2a-go v0.1.9 // indirect
 
41
  github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
42
  github.com/googleapis/gax-go/v2 v2.14.1 // indirect
43
  github.com/gosimple/unidecode v1.0.1 // indirect
space/space/space/main.go CHANGED
@@ -2,15 +2,16 @@ package main
2
 
3
  import (
4
  "api.qobiltu.id/config"
5
- cv_controller "api.qobiltu.id/controller/cv"
6
  "api.qobiltu.id/controller/health_check"
7
  "api.qobiltu.id/mail"
 
 
 
8
  "api.qobiltu.id/repositories"
9
  "api.qobiltu.id/router"
10
  "api.qobiltu.id/services"
11
  "api.qobiltu.id/utils"
12
- "api.qobiltu.id/validation"
13
- "api.qobiltu.id/worker"
14
  "github.com/hibiken/asynq"
15
  "log/slog"
16
  "net"
@@ -23,6 +24,9 @@ func main() {
23
  err := validation.New(validation.LocaleID)
24
  utils.FatalIfErr("failed to setup validator", err)
25
 
 
 
 
26
  // setup email sender
27
  emailConfig := mail.Config{
28
  Host: config.SMTP_HOST,
@@ -53,7 +57,7 @@ func main() {
53
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
54
 
55
  cvRepository := repositories.NewCVRepository(config.DB)
56
- cvService := services.NewCVService(cvRepository)
57
  cvController := cv_controller.NewCVController(cvService)
58
 
59
  // start task processor
 
2
 
3
  import (
4
  "api.qobiltu.id/config"
5
+ "api.qobiltu.id/controller/cv"
6
  "api.qobiltu.id/controller/health_check"
7
  "api.qobiltu.id/mail"
8
+ "api.qobiltu.id/pkg/storage"
9
+ "api.qobiltu.id/pkg/validation"
10
+ "api.qobiltu.id/pkg/worker"
11
  "api.qobiltu.id/repositories"
12
  "api.qobiltu.id/router"
13
  "api.qobiltu.id/services"
14
  "api.qobiltu.id/utils"
 
 
15
  "github.com/hibiken/asynq"
16
  "log/slog"
17
  "net"
 
24
  err := validation.New(validation.LocaleID)
25
  utils.FatalIfErr("failed to setup validator", err)
26
 
27
+ // setup storage
28
+ localStorage := storage.NewLocalStorage("uploads", config.APP_URL+"/storage/")
29
+
30
  // setup email sender
31
  emailConfig := mail.Config{
32
  Host: config.SMTP_HOST,
 
57
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
58
 
59
  cvRepository := repositories.NewCVRepository(config.DB)
60
+ cvService := services.NewCVService(cvRepository, localStorage)
61
  cvController := cv_controller.NewCVController(cvService)
62
 
63
  // start task processor
space/space/space/models/exception_model.go CHANGED
@@ -1,6 +1,8 @@
1
  package models
2
 
3
- import "api.qobiltu.id/validation"
 
 
4
 
5
  type Exception struct {
6
  Unauthorized bool `json:"unauthorized,omitempty"`
 
1
  package models
2
 
3
+ import (
4
+ "api.qobiltu.id/pkg/validation"
5
+ )
6
 
7
  type Exception struct {
8
  Unauthorized bool `json:"unauthorized,omitempty"`
space/space/space/models/request_model.go CHANGED
@@ -1,8 +1,10 @@
1
  package models
2
 
3
  import (
4
- "github.com/lib/pq"
5
  "time"
 
 
6
  )
7
 
8
  type LoginRequest struct {
@@ -106,6 +108,7 @@ type (
106
  MaritalStatus *string `json:"marital_status" validate:"marital_status"`
107
  LastEducation *string `json:"last_education" validate:"last_education"`
108
  LastJob *string `json:"last_job"`
 
109
  }
110
 
111
  WorshipAndReligiousUnderstandingRequest struct {
@@ -149,4 +152,13 @@ type (
149
  AccountID int64 `json:"account_id"`
150
  AchievementOrAward *string `json:"achievement_or_award"` // prestasi atau penghargaan
151
  }
 
 
 
 
 
 
 
 
 
152
  )
 
1
  package models
2
 
3
  import (
4
+ "mime/multipart"
5
  "time"
6
+
7
+ "github.com/lib/pq"
8
  )
9
 
10
  type LoginRequest struct {
 
108
  MaritalStatus *string `json:"marital_status" validate:"marital_status"`
109
  LastEducation *string `json:"last_education" validate:"last_education"`
110
  LastJob *string `json:"last_job"`
111
+ PhoneNumber *string `json:"phone_number" validate:"phone_number"`
112
  }
113
 
114
  WorshipAndReligiousUnderstandingRequest struct {
 
152
  AccountID int64 `json:"account_id"`
153
  AchievementOrAward *string `json:"achievement_or_award"` // prestasi atau penghargaan
154
  }
155
+
156
+ UploadProfileImageRequest struct {
157
+ AccountID int64
158
+ File *multipart.FileHeader
159
+ }
160
+
161
+ UploadProfileImageResponse struct {
162
+ URL string `json:"url"`
163
+ }
164
  )
space/space/space/pkg/storage/local.go ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
space/space/space/pkg/storage/storage.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package storage
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "github.com/google/uuid"
7
+ "io"
8
+ "path/filepath"
9
+ )
10
+
11
+ type FileType int
12
+
13
+ const (
14
+ ProfileImage FileType = iota
15
+ )
16
+
17
+ var (
18
+ ErrInvalidFileType = errors.New("invalid file type")
19
+ ErrFileTooLarge = errors.New("file too large")
20
+ ErrNoFileUploaded = errors.New("no file uploaded")
21
+ )
22
+
23
+ type Storage interface {
24
+ Upload(ctx context.Context, file io.ReadSeeker, path string) error
25
+ GetURL(path string) string
26
+ Delete(ctx context.Context, path string) error
27
+
28
+ ValidateExtension(fileType FileType, filename string) bool
29
+ GenerateFilename(originalName string) string
30
+ GetPath(fileType FileType, identifier string, filename string) string
31
+ }
32
+
33
+ var (
34
+ _ Storage = (*LocalStorage)(nil)
35
+ )
36
+
37
+ type BaseStorage struct {
38
+ allowedExtensions map[FileType][]string
39
+ }
40
+
41
+ func NewBaseStorage() *BaseStorage {
42
+ return &BaseStorage{
43
+ allowedExtensions: map[FileType][]string{
44
+ ProfileImage: {".jpg", ".jpeg", ".png", ".webp"},
45
+ },
46
+ }
47
+ }
48
+
49
+ func (bs *BaseStorage) ValidateExtension(fileType FileType, filename string) bool {
50
+ ext := filepath.Ext(filename)
51
+ for _, allowed := range bs.allowedExtensions[fileType] {
52
+ if ext == allowed {
53
+ return true
54
+ }
55
+ }
56
+ return false
57
+ }
58
+
59
+ func (bs *BaseStorage) GenerateFilename(originalName string) string {
60
+ ext := filepath.Ext(originalName)
61
+ return uuid.New().String() + ext
62
+ }
63
+
64
+ func (bs *BaseStorage) GetPath(fileType FileType, identifier string, filename string) string {
65
+ switch fileType {
66
+ case ProfileImage:
67
+ return filepath.Join("users", identifier, "profile", filename)
68
+ default:
69
+ return filepath.Join("misc", filename)
70
+ }
71
+ }
space/space/space/response/api_response_v2.go CHANGED
@@ -2,8 +2,8 @@ package response
2
 
3
  import (
4
  "api.qobiltu.id/models"
 
5
  "api.qobiltu.id/utils"
6
- "api.qobiltu.id/validation"
7
  "errors"
8
  "net/http"
9
 
 
2
 
3
  import (
4
  "api.qobiltu.id/models"
5
+ "api.qobiltu.id/pkg/validation"
6
  "api.qobiltu.id/utils"
 
7
  "errors"
8
  "net/http"
9
 
space/space/space/response/validation.go CHANGED
@@ -2,7 +2,7 @@ package response
2
 
3
  import (
4
  "api.qobiltu.id/models"
5
- "api.qobiltu.id/validation"
6
  )
7
 
8
  func HandleValidationError(validationErrors []validation.ErrorMessage) error {
 
2
 
3
  import (
4
  "api.qobiltu.id/models"
5
+ "api.qobiltu.id/pkg/validation"
6
  )
7
 
8
  func HandleValidationError(validationErrors []validation.ErrorMessage) error {
space/space/space/router/cv_route.go CHANGED
@@ -7,6 +7,7 @@ func (s *Server) CVRoute() {
7
  {
8
  routerGroup.POST("/account-details", s.cvController.SaveAccountDetails)
9
  routerGroup.GET("/account-details", s.cvController.GetAccountDetails)
 
10
 
11
  routerGroup.POST("/personality-and-preferences", s.cvController.SavePersonalityAndPreference)
12
  routerGroup.GET("/personality-and-preferences", s.cvController.GetPersonalityAndPreference)
 
7
  {
8
  routerGroup.POST("/account-details", s.cvController.SaveAccountDetails)
9
  routerGroup.GET("/account-details", s.cvController.GetAccountDetails)
10
+ routerGroup.POST("/account-details/avatar", s.cvController.UploadProfileImage)
11
 
12
  routerGroup.POST("/personality-and-preferences", s.cvController.SavePersonalityAndPreference)
13
  routerGroup.GET("/personality-and-preferences", s.cvController.GetPersonalityAndPreference)
space/space/space/router/router.go CHANGED
@@ -18,4 +18,5 @@ func (s *Server) setupRoutes() {
18
  // another way to register routes
19
  s.HealthCheckRoute()
20
  s.CVRoute()
 
21
  }
 
18
  // another way to register routes
19
  s.HealthCheckRoute()
20
  s.CVRoute()
21
+ s.StorageRoute()
22
  }
space/space/space/router/storage_route.go ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import (
4
+ "api.qobiltu.id/middleware"
5
+ "github.com/gin-gonic/gin"
6
+ "net/http"
7
+ "strings"
8
+ )
9
+
10
+ func (s *Server) StorageRoute() {
11
+ s.router.GET("/storage/*filepath", func(ctx *gin.Context) {
12
+ filepath := ctx.Param("filepath")
13
+
14
+ // Prevent directory listing
15
+ if filepath == "" || strings.HasSuffix(filepath, "/") {
16
+ ctx.AbortWithStatus(http.StatusForbidden)
17
+ return
18
+ }
19
+
20
+ fs := http.Dir("uploads")
21
+
22
+ ctx.FileFromFS(filepath, fs)
23
+ }).Use(middleware.AuthUser)
24
+ }
space/space/space/services/cv_service.go CHANGED
@@ -1,485 +1,568 @@
1
  package services
2
 
3
  import (
4
- "api.qobiltu.id/models"
5
- "api.qobiltu.id/repositories"
6
- "api.qobiltu.id/response"
7
- "api.qobiltu.id/validation"
8
- "context"
9
- "errors"
10
- "gorm.io/gorm"
 
 
 
11
  )
12
 
13
  type CVService interface {
14
- SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error)
15
- GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error)
16
-
17
- SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error)
18
- GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error)
19
-
20
- CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
21
- UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
22
- ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
23
- GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
24
- DeleteFamilyMember(ctx context.Context, id int64) error
25
-
26
- SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error)
27
- GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error)
28
-
29
- SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error)
30
- GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error)
31
-
32
- CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error)
33
- UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error)
34
- ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
35
- GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
36
- DeleteEducation(ctx context.Context, id int64) error
37
-
38
- CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error)
39
- UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error)
40
- ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
41
- GetJob(ctx context.Context, id int64) (*models.JobCV, error)
42
- DeleteJob(ctx context.Context, id int64) error
43
-
44
- CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error)
45
- UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error)
46
- ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
47
- GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
48
- DeleteAchievement(ctx context.Context, id int64) error
 
 
49
  }
50
 
51
  type cvService struct {
52
- cvRepository repositories.CVRepository
 
53
  }
54
 
55
- func NewCVService(cvRepository repositories.CVRepository) CVService {
56
- return &cvService{
57
- cvRepository: cvRepository,
58
- }
 
59
  }
60
 
61
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error) {
62
- if err := validation.Validate(req); err != nil {
63
- return nil, response.HandleValidationError(err)
64
- }
65
-
66
- // Ambil data lama jika ada
67
- accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, req.AccountID)
68
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
69
- return nil, response.HandleGormError(err, "Internal Server Error")
70
- }
71
-
72
- // Apply perubahan
73
- if accountDetails == nil {
74
- accountDetails = &models.AccountDetails{}
75
- }
76
-
77
- accountDetails.AccountID = uint(req.AccountID)
78
- accountDetails.FullName = req.FullName
79
- accountDetails.Gender = req.Gender
80
- accountDetails.DateOfBirth = req.DateOfBirth
81
- accountDetails.PlaceOfBirth = req.PlaceOfBirth
82
- accountDetails.Domicile = req.Domicile
83
- accountDetails.MaritalStatus = req.MaritalStatus
84
- accountDetails.LastEducation = req.LastEducation
85
- accountDetails.LastJob = req.LastJob
86
-
87
- // Simpan data
88
- res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
89
- if err != nil {
90
- return nil, response.HandleGormError(err, "Internal Server Error")
91
- }
92
-
93
- return res, nil
 
 
 
 
 
94
  }
95
 
96
  func (s *cvService) GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error) {
97
- res, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, id)
98
- if err != nil {
99
- return nil, response.HandleGormError(err, "Data diri tidak ditemukan")
100
- }
101
- return res, nil
102
  }
103
 
104
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error) {
105
- if err := validation.Validate(req); err != nil {
106
- return nil, response.HandleValidationError(err)
107
- }
108
-
109
- // Ambil data lama jika ada
110
- personalityAndPreference, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
111
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
112
- return nil, response.HandleGormError(err, "Internal Server Error")
113
- }
114
-
115
- // Apply perubahan
116
- if personalityAndPreference == nil {
117
- personalityAndPreference = &models.PersonalityAndPreferenceCV{}
118
- }
119
-
120
- personalityAndPreference.AccountID = req.AccountID
121
- personalityAndPreference.PositiveTraits = req.PositiveTraits
122
- personalityAndPreference.NegativeTraits = req.NegativeTraits
123
- personalityAndPreference.Hobbies = req.Hobbies
124
- personalityAndPreference.LifeGoals = req.LifeGoals
125
- personalityAndPreference.DailyActivities = req.DailyActivities
126
- personalityAndPreference.LeisureActivities = req.LeisureActivities
127
- personalityAndPreference.Likes = req.Likes
128
- personalityAndPreference.Dislikes = req.Dislikes
129
- personalityAndPreference.StressHandling = req.StressHandling
130
- personalityAndPreference.AngerTriggers = req.AngerTriggers
131
- personalityAndPreference.FavoriteFoodAndDrinks = req.FavoriteFoodAndDrinks
132
- personalityAndPreference.CanCook = req.CanCook
133
- personalityAndPreference.TypesOfDishesCooked = req.TypesOfDishesCooked
134
- personalityAndPreference.MonthlyExpenses = req.MonthlyExpenses
135
-
136
- res, err := s.cvRepository.SavePersonalityAndPreference(ctx, personalityAndPreference)
137
- if err != nil {
138
- return nil, response.HandleGormError(err, "Internal Server Error")
139
- }
140
-
141
- return res, nil
142
  }
143
 
144
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error) {
145
- res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, id)
146
- if err != nil {
147
- return nil, response.HandleGormError(err, "Internal Server Error")
148
- }
149
 
150
- return res, nil
151
  }
152
 
153
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
154
- if err := validation.Validate(req); err != nil {
155
- return nil, response.HandleValidationError(err)
156
- }
157
-
158
- // Mapping request ke model
159
- familyMember := &models.FamilyMemberCV{
160
- AccountID: req.AccountID,
161
- Role: req.Role,
162
- Status: req.Status,
163
- Religion: req.Religion,
164
- Job: req.Job,
165
- LastEducation: req.LastEducation,
166
- Age: req.Age,
167
- }
168
-
169
- // Simpan ke repository
170
- res, err := s.cvRepository.SaveFamilyMember(ctx, familyMember)
171
- if err != nil {
172
- return nil, response.HandleGormError(err, "Gagal menyimpan anggota keluarga")
173
- }
174
-
175
- return res, nil
176
  }
177
 
178
  func (s *cvService) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
179
- list, err := s.cvRepository.ListFamilyMember(ctx, accountID)
180
- if err != nil {
181
- return nil, response.HandleGormError(err, "Gagal mengambil daftar anggota keluarga")
182
- }
183
- return list, nil
184
  }
185
 
186
  func (s *cvService) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
187
- res, err := s.cvRepository.GetFamilyMember(ctx, id)
188
- if err != nil {
189
- return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
190
- }
191
- return res, nil
192
  }
193
 
194
  func (s *cvService) DeleteFamilyMember(ctx context.Context, id int64) error {
195
- err := s.cvRepository.DeleteFamilyMember(ctx, id)
196
- if err != nil {
197
- return response.HandleGormError(err, "Gagal menghapus anggota keluarga")
198
- }
199
- return nil
200
  }
201
 
202
  func (s *cvService) UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
203
- if err := validation.Validate(req); err != nil {
204
- return nil, response.HandleValidationError(err)
205
- }
206
-
207
- existing, err := s.cvRepository.GetFamilyMember(ctx, id)
208
- if err != nil {
209
- return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
210
- }
211
-
212
- existing.Role = req.Role
213
- existing.Status = req.Status
214
- existing.Religion = req.Religion
215
- existing.Job = req.Job
216
- existing.LastEducation = req.LastEducation
217
- existing.Age = req.Age
218
-
219
- updated, err := s.cvRepository.SaveFamilyMember(ctx, existing)
220
- if err != nil {
221
- return nil, response.HandleGormError(err, "Gagal memperbarui anggota keluarga")
222
- }
223
-
224
- return updated, nil
225
  }
226
 
227
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
228
- if err := validation.Validate(req); err != nil {
229
- return nil, response.HandleValidationError(err)
230
- }
231
-
232
- // Cek apakah data sudah ada berdasarkan account_id
233
- existing, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
234
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
235
- return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data fisik dan kesehatan")
236
- }
237
-
238
- // Jika belum ada, buat objek baru
239
- if existing == nil {
240
- existing = &models.PhysicalAndHealthCV{}
241
- }
242
-
243
- // Mapping field dari request
244
- existing.AccountID = req.AccountID
245
- existing.HeightInCm = req.HeightInCm
246
- existing.WeightInKg = req.WeightInKg
247
- existing.BodyShape = req.BodyShape
248
- existing.SkinColor = req.SkinColor
249
- existing.HairType = req.HairType
250
- existing.MedicalHistory = req.MedicalHistory
251
- existing.PhysicalDisorder = req.PhysicalDisorder
252
- existing.PhysicalTraits = req.PhysicalTraits
253
-
254
- // Simpan data
255
- res, err := s.cvRepository.SavePhysicalAndHealth(ctx, existing)
256
- if err != nil {
257
- return nil, response.HandleGormError(err, "Gagal menyimpan data fisik dan kesehatan")
258
- }
259
-
260
- return res, nil
261
  }
262
 
263
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error) {
264
- res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, id)
265
- if err != nil {
266
- return nil, response.HandleGormError(err, "Data fisik dan kesehatan tidak ditemukan")
267
- }
268
- return res, nil
269
  }
270
 
271
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
272
- if err := validation.Validate(req); err != nil {
273
- return nil, response.HandleValidationError(err)
274
- }
275
-
276
- // Cek apakah data sudah ada berdasarkan account_id
277
- worshipAndReligiousUnderstanding, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
278
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
279
- return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data agama dan pemahaman agama")
280
- }
281
-
282
- // Jika belum ada, buat objek baru
283
- if worshipAndReligiousUnderstanding == nil {
284
- worshipAndReligiousUnderstanding = &models.WorshipAndReligiousUnderstandingCV{}
285
- }
286
-
287
- // Mapping field dari request
288
- worshipAndReligiousUnderstanding.AccountID = req.AccountID
289
- worshipAndReligiousUnderstanding.ObligatoryPrayer = req.ObligatoryPrayer
290
- worshipAndReligiousUnderstanding.CongregationalPrayer = req.CongregationalPrayer
291
- worshipAndReligiousUnderstanding.TahajjudPrayer = req.TahajjudPrayer
292
- worshipAndReligiousUnderstanding.DhuhaPrayer = req.DhuhaPrayer
293
- worshipAndReligiousUnderstanding.QuranMemorization = req.QuranMemorization
294
- worshipAndReligiousUnderstanding.QuranReadingAbility = req.QuranReadingAbility
295
- worshipAndReligiousUnderstanding.DaudFasting = req.DaudFasting
296
- worshipAndReligiousUnderstanding.AyyamulBidhFasting = req.AyyamulBidhFasting
297
- worshipAndReligiousUnderstanding.HajjOrUmrah = req.HajjOrUmrah
298
- worshipAndReligiousUnderstanding.ListeningToMusic = req.ListeningToMusic
299
- worshipAndReligiousUnderstanding.OpinionOnIkhtilat = req.OpinionOnIkhtilat
300
- worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram = req.OpinionOnTouchingNonMahram
301
- worshipAndReligiousUnderstanding.OpinionOnVeil = req.OpinionOnVeil
302
- worshipAndReligiousUnderstanding.WeeklyReligiousStudies = req.WeeklyReligiousStudies
303
- worshipAndReligiousUnderstanding.FollowedUstadz = req.FollowedUstadz
304
-
305
- // Simpan data
306
- res, err := s.cvRepository.SaveWorshipAndReligiousUnderstanding(ctx, worshipAndReligiousUnderstanding)
307
- if err != nil {
308
- return nil, response.HandleGormError(err, "Internal Server Error")
309
- }
310
-
311
- return res, nil
312
  }
313
 
314
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
315
- res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, id)
316
- if err != nil {
317
- return nil, response.HandleGormError(err, "Data agama dan pemahaman agama tidak ditemukan")
318
- }
319
- return res, nil
320
  }
321
 
322
  func (s *cvService) CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error) {
323
- if err := validation.Validate(req); err != nil {
324
- return nil, response.HandleValidationError(err)
325
- }
326
-
327
- edu := &models.EducationCV{
328
- AccountID: req.AccountID,
329
- LastEducation: req.LastEducation,
330
- EducationInstitute: req.EducationInstitute,
331
- EducationMajor: req.EducationMajor,
332
- YearStart: req.YearStart,
333
- YearGraduate: req.YearGraduate,
334
- }
335
-
336
- res, err := s.cvRepository.SaveEducation(ctx, edu)
337
- if err != nil {
338
- return nil, response.HandleGormError(err, "Gagal menambahkan data pendidikan")
339
- }
340
-
341
- return res, nil
342
  }
343
 
344
  func (s *cvService) UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error) {
345
- if err := validation.Validate(req); err != nil {
346
- return nil, response.HandleValidationError(err)
347
- }
348
-
349
- edu, err := s.cvRepository.GetEducation(ctx, id)
350
- if err != nil {
351
- return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
352
- }
353
-
354
- edu.LastEducation = req.LastEducation
355
- edu.EducationInstitute = req.EducationInstitute
356
- edu.EducationMajor = req.EducationMajor
357
- edu.YearStart = req.YearStart
358
- edu.YearGraduate = req.YearGraduate
359
-
360
- res, err := s.cvRepository.SaveEducation(ctx, edu)
361
- if err != nil {
362
- return nil, response.HandleGormError(err, "Gagal memperbarui data pendidikan")
363
- }
364
- return res, nil
365
  }
366
 
367
  func (s *cvService) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
368
- return s.cvRepository.ListEducation(ctx, accountID)
369
  }
370
 
371
  func (s *cvService) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
372
- edu, err := s.cvRepository.GetEducation(ctx, id)
373
- if err != nil {
374
- return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
375
- }
376
- return edu, nil
377
  }
378
 
379
  func (s *cvService) DeleteEducation(ctx context.Context, id int64) error {
380
- return s.cvRepository.DeleteEducation(ctx, id)
381
  }
382
 
383
  func (s *cvService) CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error) {
384
- if err := validation.Validate(req); err != nil {
385
- return nil, response.HandleValidationError(err)
386
- }
387
-
388
- job := &models.JobCV{
389
- AccountID: req.AccountID,
390
- InstitutionName: req.InstitutionName,
391
- CurrentJob: req.CurrentJob,
392
- YearStartedWorking: req.YearStartedWorking,
393
- MonthlyIncome: req.MonthlyIncome,
394
- IncomeSources: req.IncomeSources,
395
- }
396
- res, err := s.cvRepository.SaveJob(ctx, job)
397
- if err != nil {
398
- return nil, response.HandleGormError(err, "Gagal menambahkan data pekerjaan")
399
- }
400
- return res, nil
401
  }
402
 
403
  func (s *cvService) UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error) {
404
- if err := validation.Validate(req); err != nil {
405
- return nil, response.HandleValidationError(err)
406
- }
407
-
408
- job, err := s.cvRepository.GetJob(ctx, id)
409
- if err != nil {
410
- return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
411
- }
412
-
413
- job.InstitutionName = req.InstitutionName
414
- job.CurrentJob = req.CurrentJob
415
- job.YearStartedWorking = req.YearStartedWorking
416
- job.MonthlyIncome = req.MonthlyIncome
417
- job.IncomeSources = req.IncomeSources
418
-
419
- res, err := s.cvRepository.SaveJob(ctx, job)
420
- if err != nil {
421
- return nil, response.HandleGormError(err, "Gagal memperbarui data pekerjaan")
422
- }
423
- return res, nil
424
  }
425
 
426
  func (s *cvService) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
427
- return s.cvRepository.ListJob(ctx, accountID)
428
  }
429
 
430
  func (s *cvService) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
431
- job, err := s.cvRepository.GetJob(ctx, id)
432
- if err != nil {
433
- return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
434
- }
435
- return job, nil
436
  }
437
 
438
  func (s *cvService) DeleteJob(ctx context.Context, id int64) error {
439
- return s.cvRepository.DeleteJob(ctx, id)
440
  }
441
 
442
  func (s *cvService) CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error) {
443
- ach := &models.AchievementCV{
444
- AccountID: req.AccountID,
445
- AchievementOrAward: req.AchievementOrAward,
446
- }
447
- res, err := s.cvRepository.SaveAchievement(ctx, ach)
448
- if err != nil {
449
- return nil, response.HandleGormError(err, "Gagal menambahkan data prestasi")
450
- }
451
-
452
- return res, nil
453
  }
454
 
455
  func (s *cvService) UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error) {
456
- ach, err := s.cvRepository.GetAchievement(ctx, id)
457
- if err != nil {
458
- return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
459
- }
460
 
461
- ach.AchievementOrAward = req.AchievementOrAward
462
 
463
- res, err := s.cvRepository.SaveAchievement(ctx, ach)
464
- if err != nil {
465
- return nil, response.HandleGormError(err, "Gagal memperbarui data prestasi")
466
- }
467
 
468
- return res, nil
469
  }
470
 
471
  func (s *cvService) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
472
- return s.cvRepository.ListAchievement(ctx, accountID)
473
  }
474
 
475
  func (s *cvService) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
476
- ach, err := s.cvRepository.GetAchievement(ctx, id)
477
- if err != nil {
478
- return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
479
- }
480
- return ach, nil
481
  }
482
 
483
  func (s *cvService) DeleteAchievement(ctx context.Context, id int64) error {
484
- return s.cvRepository.DeleteAchievement(ctx, id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
 
1
  package services
2
 
3
  import (
4
+ "context"
5
+ "errors"
6
+ "strconv"
7
+
8
+ "api.qobiltu.id/models"
9
+ "api.qobiltu.id/pkg/storage"
10
+ "api.qobiltu.id/pkg/validation"
11
+ "api.qobiltu.id/repositories"
12
+ "api.qobiltu.id/response"
13
+ "gorm.io/gorm"
14
  )
15
 
16
  type CVService interface {
17
+ SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error)
18
+ GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error)
19
+
20
+ SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error)
21
+ GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error)
22
+
23
+ CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
24
+ UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
25
+ ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
26
+ GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
27
+ DeleteFamilyMember(ctx context.Context, id int64) error
28
+
29
+ SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error)
30
+ GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error)
31
+
32
+ SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error)
33
+ GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error)
34
+
35
+ CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error)
36
+ UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error)
37
+ ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
38
+ GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
39
+ DeleteEducation(ctx context.Context, id int64) error
40
+
41
+ CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error)
42
+ UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error)
43
+ ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
44
+ GetJob(ctx context.Context, id int64) (*models.JobCV, error)
45
+ DeleteJob(ctx context.Context, id int64) error
46
+
47
+ CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error)
48
+ UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error)
49
+ ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
50
+ GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
51
+ DeleteAchievement(ctx context.Context, id int64) error
52
+
53
+ UploadProfileImage(ctx context.Context, req *models.UploadProfileImageRequest) (*models.UploadProfileImageResponse, error)
54
  }
55
 
56
  type cvService struct {
57
+ cvRepository repositories.CVRepository
58
+ storage storage.Storage
59
  }
60
 
61
+ func NewCVService(cvRepository repositories.CVRepository, storage storage.Storage) CVService {
62
+ return &cvService{
63
+ cvRepository: cvRepository,
64
+ storage: storage,
65
+ }
66
  }
67
 
68
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error) {
69
+ if err := validation.Validate(req); err != nil {
70
+ return nil, response.HandleValidationError(err)
71
+ }
72
+
73
+ // Ambil data lama jika ada
74
+ accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, req.AccountID)
75
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
76
+ return nil, response.HandleGormError(err, "Internal Server Error")
77
+ }
78
+
79
+ // Apply perubahan
80
+ if accountDetails == nil {
81
+ accountDetails = &models.AccountDetails{}
82
+ }
83
+
84
+ accountDetails.AccountID = uint(req.AccountID)
85
+ accountDetails.FullName = req.FullName
86
+ accountDetails.Gender = req.Gender
87
+ accountDetails.DateOfBirth = req.DateOfBirth
88
+ accountDetails.PlaceOfBirth = req.PlaceOfBirth
89
+ accountDetails.Domicile = req.Domicile
90
+ accountDetails.MaritalStatus = req.MaritalStatus
91
+ accountDetails.LastEducation = req.LastEducation
92
+ accountDetails.LastJob = req.LastJob
93
+
94
+ if req.PhoneNumber != nil {
95
+ sanitizedPhone := validation.SanitizePhoneNumber(*req.PhoneNumber)
96
+ accountDetails.PhoneNumber = &sanitizedPhone
97
+ }
98
+
99
+ // Simpan data
100
+ res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
101
+ if err != nil {
102
+ return nil, response.HandleGormError(err, "Internal Server Error")
103
+ }
104
+
105
+ return res, nil
106
  }
107
 
108
  func (s *cvService) GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error) {
109
+ res, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, id)
110
+ if err != nil {
111
+ return nil, response.HandleGormError(err, "Data diri tidak ditemukan")
112
+ }
113
+ return res, nil
114
  }
115
 
116
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error) {
117
+ if err := validation.Validate(req); err != nil {
118
+ return nil, response.HandleValidationError(err)
119
+ }
120
+
121
+ // Ambil data lama jika ada
122
+ personalityAndPreference, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
123
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
124
+ return nil, response.HandleGormError(err, "Internal Server Error")
125
+ }
126
+
127
+ // Apply perubahan
128
+ if personalityAndPreference == nil {
129
+ personalityAndPreference = &models.PersonalityAndPreferenceCV{}
130
+ }
131
+
132
+ personalityAndPreference.AccountID = req.AccountID
133
+ personalityAndPreference.PositiveTraits = req.PositiveTraits
134
+ personalityAndPreference.NegativeTraits = req.NegativeTraits
135
+ personalityAndPreference.Hobbies = req.Hobbies
136
+ personalityAndPreference.LifeGoals = req.LifeGoals
137
+ personalityAndPreference.DailyActivities = req.DailyActivities
138
+ personalityAndPreference.LeisureActivities = req.LeisureActivities
139
+ personalityAndPreference.Likes = req.Likes
140
+ personalityAndPreference.Dislikes = req.Dislikes
141
+ personalityAndPreference.StressHandling = req.StressHandling
142
+ personalityAndPreference.AngerTriggers = req.AngerTriggers
143
+ personalityAndPreference.FavoriteFoodAndDrinks = req.FavoriteFoodAndDrinks
144
+ personalityAndPreference.CanCook = req.CanCook
145
+ personalityAndPreference.TypesOfDishesCooked = req.TypesOfDishesCooked
146
+ personalityAndPreference.MonthlyExpenses = req.MonthlyExpenses
147
+
148
+ res, err := s.cvRepository.SavePersonalityAndPreference(ctx, personalityAndPreference)
149
+ if err != nil {
150
+ return nil, response.HandleGormError(err, "Internal Server Error")
151
+ }
152
+
153
+ return res, nil
154
  }
155
 
156
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error) {
157
+ res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, id)
158
+ if err != nil {
159
+ return nil, response.HandleGormError(err, "Internal Server Error")
160
+ }
161
 
162
+ return res, nil
163
  }
164
 
165
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
166
+ if err := validation.Validate(req); err != nil {
167
+ return nil, response.HandleValidationError(err)
168
+ }
169
+
170
+ // Mapping request ke model
171
+ familyMember := &models.FamilyMemberCV{
172
+ AccountID: req.AccountID,
173
+ Role: req.Role,
174
+ Status: req.Status,
175
+ Religion: req.Religion,
176
+ Job: req.Job,
177
+ LastEducation: req.LastEducation,
178
+ Age: req.Age,
179
+ }
180
+
181
+ // Simpan ke repository
182
+ res, err := s.cvRepository.SaveFamilyMember(ctx, familyMember)
183
+ if err != nil {
184
+ return nil, response.HandleGormError(err, "Gagal menyimpan anggota keluarga")
185
+ }
186
+
187
+ return res, nil
188
  }
189
 
190
  func (s *cvService) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
191
+ list, err := s.cvRepository.ListFamilyMember(ctx, accountID)
192
+ if err != nil {
193
+ return nil, response.HandleGormError(err, "Gagal mengambil daftar anggota keluarga")
194
+ }
195
+ return list, nil
196
  }
197
 
198
  func (s *cvService) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
199
+ res, err := s.cvRepository.GetFamilyMember(ctx, id)
200
+ if err != nil {
201
+ return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
202
+ }
203
+ return res, nil
204
  }
205
 
206
  func (s *cvService) DeleteFamilyMember(ctx context.Context, id int64) error {
207
+ err := s.cvRepository.DeleteFamilyMember(ctx, id)
208
+ if err != nil {
209
+ return response.HandleGormError(err, "Gagal menghapus anggota keluarga")
210
+ }
211
+ return nil
212
  }
213
 
214
  func (s *cvService) UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
215
+ if err := validation.Validate(req); err != nil {
216
+ return nil, response.HandleValidationError(err)
217
+ }
218
+
219
+ existing, err := s.cvRepository.GetFamilyMember(ctx, id)
220
+ if err != nil {
221
+ return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
222
+ }
223
+
224
+ existing.Role = req.Role
225
+ existing.Status = req.Status
226
+ existing.Religion = req.Religion
227
+ existing.Job = req.Job
228
+ existing.LastEducation = req.LastEducation
229
+ existing.Age = req.Age
230
+
231
+ updated, err := s.cvRepository.SaveFamilyMember(ctx, existing)
232
+ if err != nil {
233
+ return nil, response.HandleGormError(err, "Gagal memperbarui anggota keluarga")
234
+ }
235
+
236
+ return updated, nil
237
  }
238
 
239
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
240
+ if err := validation.Validate(req); err != nil {
241
+ return nil, response.HandleValidationError(err)
242
+ }
243
+
244
+ // Cek apakah data sudah ada berdasarkan account_id
245
+ existing, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
246
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
247
+ return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data fisik dan kesehatan")
248
+ }
249
+
250
+ // Jika belum ada, buat objek baru
251
+ if existing == nil {
252
+ existing = &models.PhysicalAndHealthCV{}
253
+ }
254
+
255
+ // Mapping field dari request
256
+ existing.AccountID = req.AccountID
257
+ existing.HeightInCm = req.HeightInCm
258
+ existing.WeightInKg = req.WeightInKg
259
+ existing.BodyShape = req.BodyShape
260
+ existing.SkinColor = req.SkinColor
261
+ existing.HairType = req.HairType
262
+ existing.MedicalHistory = req.MedicalHistory
263
+ existing.PhysicalDisorder = req.PhysicalDisorder
264
+ existing.PhysicalTraits = req.PhysicalTraits
265
+
266
+ // Simpan data
267
+ res, err := s.cvRepository.SavePhysicalAndHealth(ctx, existing)
268
+ if err != nil {
269
+ return nil, response.HandleGormError(err, "Gagal menyimpan data fisik dan kesehatan")
270
+ }
271
+
272
+ return res, nil
273
  }
274
 
275
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error) {
276
+ res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, id)
277
+ if err != nil {
278
+ return nil, response.HandleGormError(err, "Data fisik dan kesehatan tidak ditemukan")
279
+ }
280
+ return res, nil
281
  }
282
 
283
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
284
+ if err := validation.Validate(req); err != nil {
285
+ return nil, response.HandleValidationError(err)
286
+ }
287
+
288
+ // Cek apakah data sudah ada berdasarkan account_id
289
+ worshipAndReligiousUnderstanding, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
290
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
291
+ return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data agama dan pemahaman agama")
292
+ }
293
+
294
+ // Jika belum ada, buat objek baru
295
+ if worshipAndReligiousUnderstanding == nil {
296
+ worshipAndReligiousUnderstanding = &models.WorshipAndReligiousUnderstandingCV{}
297
+ }
298
+
299
+ // Mapping field dari request
300
+ worshipAndReligiousUnderstanding.AccountID = req.AccountID
301
+ worshipAndReligiousUnderstanding.ObligatoryPrayer = req.ObligatoryPrayer
302
+ worshipAndReligiousUnderstanding.CongregationalPrayer = req.CongregationalPrayer
303
+ worshipAndReligiousUnderstanding.TahajjudPrayer = req.TahajjudPrayer
304
+ worshipAndReligiousUnderstanding.DhuhaPrayer = req.DhuhaPrayer
305
+ worshipAndReligiousUnderstanding.QuranMemorization = req.QuranMemorization
306
+ worshipAndReligiousUnderstanding.QuranReadingAbility = req.QuranReadingAbility
307
+ worshipAndReligiousUnderstanding.DaudFasting = req.DaudFasting
308
+ worshipAndReligiousUnderstanding.AyyamulBidhFasting = req.AyyamulBidhFasting
309
+ worshipAndReligiousUnderstanding.HajjOrUmrah = req.HajjOrUmrah
310
+ worshipAndReligiousUnderstanding.ListeningToMusic = req.ListeningToMusic
311
+ worshipAndReligiousUnderstanding.OpinionOnIkhtilat = req.OpinionOnIkhtilat
312
+ worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram = req.OpinionOnTouchingNonMahram
313
+ worshipAndReligiousUnderstanding.OpinionOnVeil = req.OpinionOnVeil
314
+ worshipAndReligiousUnderstanding.WeeklyReligiousStudies = req.WeeklyReligiousStudies
315
+ worshipAndReligiousUnderstanding.FollowedUstadz = req.FollowedUstadz
316
+
317
+ // Simpan data
318
+ res, err := s.cvRepository.SaveWorshipAndReligiousUnderstanding(ctx, worshipAndReligiousUnderstanding)
319
+ if err != nil {
320
+ return nil, response.HandleGormError(err, "Internal Server Error")
321
+ }
322
+
323
+ return res, nil
324
  }
325
 
326
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
327
+ res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, id)
328
+ if err != nil {
329
+ return nil, response.HandleGormError(err, "Data agama dan pemahaman agama tidak ditemukan")
330
+ }
331
+ return res, nil
332
  }
333
 
334
  func (s *cvService) CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error) {
335
+ if err := validation.Validate(req); err != nil {
336
+ return nil, response.HandleValidationError(err)
337
+ }
338
+
339
+ edu := &models.EducationCV{
340
+ AccountID: req.AccountID,
341
+ LastEducation: req.LastEducation,
342
+ EducationInstitute: req.EducationInstitute,
343
+ EducationMajor: req.EducationMajor,
344
+ YearStart: req.YearStart,
345
+ YearGraduate: req.YearGraduate,
346
+ }
347
+
348
+ res, err := s.cvRepository.SaveEducation(ctx, edu)
349
+ if err != nil {
350
+ return nil, response.HandleGormError(err, "Gagal menambahkan data pendidikan")
351
+ }
352
+
353
+ return res, nil
354
  }
355
 
356
  func (s *cvService) UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error) {
357
+ if err := validation.Validate(req); err != nil {
358
+ return nil, response.HandleValidationError(err)
359
+ }
360
+
361
+ edu, err := s.cvRepository.GetEducation(ctx, id)
362
+ if err != nil {
363
+ return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
364
+ }
365
+
366
+ edu.LastEducation = req.LastEducation
367
+ edu.EducationInstitute = req.EducationInstitute
368
+ edu.EducationMajor = req.EducationMajor
369
+ edu.YearStart = req.YearStart
370
+ edu.YearGraduate = req.YearGraduate
371
+
372
+ res, err := s.cvRepository.SaveEducation(ctx, edu)
373
+ if err != nil {
374
+ return nil, response.HandleGormError(err, "Gagal memperbarui data pendidikan")
375
+ }
376
+ return res, nil
377
  }
378
 
379
  func (s *cvService) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
380
+ return s.cvRepository.ListEducation(ctx, accountID)
381
  }
382
 
383
  func (s *cvService) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
384
+ edu, err := s.cvRepository.GetEducation(ctx, id)
385
+ if err != nil {
386
+ return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
387
+ }
388
+ return edu, nil
389
  }
390
 
391
  func (s *cvService) DeleteEducation(ctx context.Context, id int64) error {
392
+ return s.cvRepository.DeleteEducation(ctx, id)
393
  }
394
 
395
  func (s *cvService) CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error) {
396
+ if err := validation.Validate(req); err != nil {
397
+ return nil, response.HandleValidationError(err)
398
+ }
399
+
400
+ job := &models.JobCV{
401
+ AccountID: req.AccountID,
402
+ InstitutionName: req.InstitutionName,
403
+ CurrentJob: req.CurrentJob,
404
+ YearStartedWorking: req.YearStartedWorking,
405
+ MonthlyIncome: req.MonthlyIncome,
406
+ IncomeSources: req.IncomeSources,
407
+ }
408
+ res, err := s.cvRepository.SaveJob(ctx, job)
409
+ if err != nil {
410
+ return nil, response.HandleGormError(err, "Gagal menambahkan data pekerjaan")
411
+ }
412
+ return res, nil
413
  }
414
 
415
  func (s *cvService) UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error) {
416
+ if err := validation.Validate(req); err != nil {
417
+ return nil, response.HandleValidationError(err)
418
+ }
419
+
420
+ job, err := s.cvRepository.GetJob(ctx, id)
421
+ if err != nil {
422
+ return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
423
+ }
424
+
425
+ job.InstitutionName = req.InstitutionName
426
+ job.CurrentJob = req.CurrentJob
427
+ job.YearStartedWorking = req.YearStartedWorking
428
+ job.MonthlyIncome = req.MonthlyIncome
429
+ job.IncomeSources = req.IncomeSources
430
+
431
+ res, err := s.cvRepository.SaveJob(ctx, job)
432
+ if err != nil {
433
+ return nil, response.HandleGormError(err, "Gagal memperbarui data pekerjaan")
434
+ }
435
+ return res, nil
436
  }
437
 
438
  func (s *cvService) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
439
+ return s.cvRepository.ListJob(ctx, accountID)
440
  }
441
 
442
  func (s *cvService) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
443
+ job, err := s.cvRepository.GetJob(ctx, id)
444
+ if err != nil {
445
+ return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
446
+ }
447
+ return job, nil
448
  }
449
 
450
  func (s *cvService) DeleteJob(ctx context.Context, id int64) error {
451
+ return s.cvRepository.DeleteJob(ctx, id)
452
  }
453
 
454
  func (s *cvService) CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error) {
455
+ ach := &models.AchievementCV{
456
+ AccountID: req.AccountID,
457
+ AchievementOrAward: req.AchievementOrAward,
458
+ }
459
+ res, err := s.cvRepository.SaveAchievement(ctx, ach)
460
+ if err != nil {
461
+ return nil, response.HandleGormError(err, "Gagal menambahkan data prestasi")
462
+ }
463
+
464
+ return res, nil
465
  }
466
 
467
  func (s *cvService) UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error) {
468
+ ach, err := s.cvRepository.GetAchievement(ctx, id)
469
+ if err != nil {
470
+ return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
471
+ }
472
 
473
+ ach.AchievementOrAward = req.AchievementOrAward
474
 
475
+ res, err := s.cvRepository.SaveAchievement(ctx, ach)
476
+ if err != nil {
477
+ return nil, response.HandleGormError(err, "Gagal memperbarui data prestasi")
478
+ }
479
 
480
+ return res, nil
481
  }
482
 
483
  func (s *cvService) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
484
+ return s.cvRepository.ListAchievement(ctx, accountID)
485
  }
486
 
487
  func (s *cvService) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
488
+ ach, err := s.cvRepository.GetAchievement(ctx, id)
489
+ if err != nil {
490
+ return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
491
+ }
492
+ return ach, nil
493
  }
494
 
495
  func (s *cvService) DeleteAchievement(ctx context.Context, id int64) error {
496
+ return s.cvRepository.DeleteAchievement(ctx, id)
497
+ }
498
+
499
+ func (s *cvService) UploadProfileImage(ctx context.Context, req *models.UploadProfileImageRequest) (*models.UploadProfileImageResponse, error) {
500
+ if req.File == nil {
501
+ return nil, models.Exception{
502
+ BadRequest: true,
503
+ Message: "No file uploaded",
504
+ Err: storage.ErrNoFileUploaded,
505
+ }
506
+ }
507
+
508
+ var maxFileSize int64 = 5 << 20 // 5MB
509
+
510
+ if req.File.Size > maxFileSize {
511
+ return nil, models.Exception{
512
+ BadRequest: true,
513
+ Message: "File too large, max size is 5MB",
514
+ Err: storage.ErrFileTooLarge,
515
+ }
516
+ }
517
+
518
+ if !s.storage.ValidateExtension(storage.ProfileImage, req.File.Filename) {
519
+ return nil, models.Exception{
520
+ BadRequest: true,
521
+ Message: "Invalid file extension",
522
+ Err: storage.ErrInvalidFileType,
523
+ }
524
+ }
525
+
526
+ accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, req.AccountID)
527
+ if err != nil {
528
+ return nil, response.HandleGormError(err, "Internal Server Error")
529
+ }
530
+
531
+ filename := s.storage.GenerateFilename(req.File.Filename)
532
+ path := s.storage.GetPath(storage.ProfileImage, strconv.Itoa(int(req.AccountID)), filename)
533
+
534
+ file, err := req.File.Open()
535
+ if err != nil {
536
+ return nil, models.Exception{
537
+ InternalServerError: true,
538
+ Message: "Internal Server Error",
539
+ Err: err,
540
+ }
541
+ }
542
+ defer file.Close()
543
+
544
+ if err := s.storage.Upload(ctx, file, path); err != nil {
545
+ return nil, models.Exception{
546
+ InternalServerError: true,
547
+ Message: "Internal Server Error",
548
+ Err: err,
549
+ }
550
+ }
551
+
552
+ // remove old avatar
553
+ if accountDetails.Avatar != nil {
554
+ s.storage.Delete(ctx, *accountDetails.Avatar)
555
+ }
556
+
557
+ url := s.storage.GetURL(path)
558
+ accountDetails.Avatar = &url
559
+
560
+ _, err = s.cvRepository.SaveAccountDetails(ctx, accountDetails)
561
+ if err != nil {
562
+ return nil, response.HandleGormError(err, "Internal Server Error")
563
+ }
564
+
565
+ return &models.UploadProfileImageResponse{
566
+ URL: url,
567
+ }, nil
568
  }
space/space/space/services/email_verification_service.go CHANGED
@@ -1,101 +1,101 @@
1
  package services
2
 
3
  import (
4
- "api.qobiltu.id/utils"
5
- "api.qobiltu.id/worker"
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"
14
- uuid "github.com/satori/go.uuid"
15
  )
16
 
17
  type EmailVerificationService struct {
18
- Service[models.EmailVerification, models.EmailVerification]
19
  }
20
 
21
  func (s *EmailVerificationService) Create() {
22
- accountRepo := repositories.GetAccountById(s.Constructor.AccountID)
23
- if accountRepo.NoRecord {
24
- s.Error = accountRepo.RowsError
25
- s.Exception.DataNotFound = true
26
- s.Exception.Message = "There is no account data with given credentials!"
27
- return
28
- }
29
 
30
- token, err := utils.GenerateToken()
31
- if err != nil {
32
- s.Error = err
33
- s.Exception.InternalServerError = true
34
- s.Exception.Message = "failed to generate token for email verification"
35
- return
36
- }
37
 
38
- remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
39
- dueTime := CalculateDueTime(remainingTime)
40
- s.Constructor.UUID = uuid.NewV4()
41
- repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, uint(token))
42
- s.Error = repo.RowsError
43
- s.Result = repo.Result
44
- if s.Error != nil {
45
- return
46
- }
47
 
48
- err = worker.AsyncTaskDistributor.DistributeTaskSendVerifyEmail(
49
- context.Background(),
50
- &worker.PayloadSendVerifyEmail{
51
- EmailAddress: accountRepo.Result.Email,
52
- VerificationCode: strconv.Itoa(int(token)),
53
- ExpirationInMinutes: int(remainingTime.Minutes()),
54
- Subject: worker.TaskSendVerifyEmailSubject,
55
- },
56
- []asynq.Option{
57
- asynq.MaxRetry(worker.TaskSendVerifyEmailMaxRetry),
58
- asynq.Queue(worker.Critical),
59
- }...)
60
- if err != nil {
61
- s.Error = err
62
- s.Exception.InternalServerError = true
63
- s.Exception.Message = "failed to send email verification"
64
- return
65
- }
66
  }
67
 
68
  func (s *EmailVerificationService) Validate() {
69
- repo := repositories.GetEmailVerification(s.Constructor.AccountID, s.Constructor.Token)
70
- s.Error = repo.RowsError
71
 
72
- if repo.NoRecord {
73
- s.Exception.DataNotFound = true
74
- s.Exception.Message = "Invalid token!"
75
- return
76
- }
77
 
78
- if repo.Result.ExpiredAt.Before(time.Now()) {
79
- s.Exception.Unauthorized = true
80
- s.Exception.Message = "Token has expired!"
81
- repositories.UpdateExpiredEmailVerification(s.Constructor.UUID)
82
- s.Delete()
83
- return
84
- }
85
- account := repositories.GetAccountById(repo.Result.AccountID)
86
- account.Result.IsEmailVerified = true
87
 
88
- repositories.UpdateAccount(account.Result)
89
- s.Result = repo.Result
90
  }
91
 
92
  func (s *EmailVerificationService) Delete() {
93
- repo := repositories.DeleteEmailVerification(s.Constructor.Token)
94
- s.Error = repo.RowsError
95
- if repo.NoRecord {
96
- s.Exception.DataNotFound = true
97
- s.Exception.Message = "Invalid token!"
98
- return
99
- }
100
- s.Result = repo.Result
101
  }
 
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"
14
+ uuid "github.com/satori/go.uuid"
15
  )
16
 
17
  type EmailVerificationService struct {
18
+ Service[models.EmailVerification, models.EmailVerification]
19
  }
20
 
21
  func (s *EmailVerificationService) Create() {
22
+ accountRepo := repositories.GetAccountById(s.Constructor.AccountID)
23
+ if accountRepo.NoRecord {
24
+ s.Error = accountRepo.RowsError
25
+ s.Exception.DataNotFound = true
26
+ s.Exception.Message = "There is no account data with given credentials!"
27
+ return
28
+ }
29
 
30
+ token, err := utils.GenerateToken()
31
+ if err != nil {
32
+ s.Error = err
33
+ s.Exception.InternalServerError = true
34
+ s.Exception.Message = "failed to generate token for email verification"
35
+ return
36
+ }
37
 
38
+ remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Hour
39
+ dueTime := CalculateDueTime(remainingTime)
40
+ s.Constructor.UUID = uuid.NewV4()
41
+ repo := repositories.CreateEmailVerification(s.Constructor.UUID, s.Constructor.AccountID, dueTime, uint(token))
42
+ s.Error = repo.RowsError
43
+ s.Result = repo.Result
44
+ if s.Error != nil {
45
+ return
46
+ }
47
 
48
+ err = worker.AsyncTaskDistributor.DistributeTaskSendVerifyEmail(
49
+ context.Background(),
50
+ &worker.PayloadSendVerifyEmail{
51
+ EmailAddress: accountRepo.Result.Email,
52
+ VerificationCode: strconv.Itoa(int(token)),
53
+ ExpirationInMinutes: int(remainingTime.Minutes()),
54
+ Subject: worker.TaskSendVerifyEmailSubject,
55
+ },
56
+ []asynq.Option{
57
+ asynq.MaxRetry(worker.TaskSendVerifyEmailMaxRetry),
58
+ asynq.Queue(worker.Critical),
59
+ }...)
60
+ if err != nil {
61
+ s.Error = err
62
+ s.Exception.InternalServerError = true
63
+ s.Exception.Message = "failed to send email verification"
64
+ return
65
+ }
66
  }
67
 
68
  func (s *EmailVerificationService) Validate() {
69
+ repo := repositories.GetEmailVerification(s.Constructor.AccountID, s.Constructor.Token)
70
+ s.Error = repo.RowsError
71
 
72
+ if repo.NoRecord {
73
+ s.Exception.DataNotFound = true
74
+ s.Exception.Message = "Invalid token!"
75
+ return
76
+ }
77
 
78
+ if repo.Result.ExpiredAt.Before(time.Now()) {
79
+ s.Exception.Unauthorized = true
80
+ s.Exception.Message = "Token has expired!"
81
+ repositories.UpdateExpiredEmailVerification(s.Constructor.UUID)
82
+ s.Delete()
83
+ return
84
+ }
85
+ account := repositories.GetAccountById(repo.Result.AccountID)
86
+ account.Result.IsEmailVerified = true
87
 
88
+ repositories.UpdateAccount(account.Result)
89
+ s.Result = repo.Result
90
  }
91
 
92
  func (s *EmailVerificationService) Delete() {
93
+ repo := repositories.DeleteEmailVerification(s.Constructor.Token)
94
+ s.Error = repo.RowsError
95
+ if repo.NoRecord {
96
+ s.Exception.DataNotFound = true
97
+ s.Exception.Message = "Invalid token!"
98
+ return
99
+ }
100
+ s.Result = repo.Result
101
  }
space/space/space/services/forgot_password_service.go CHANGED
@@ -1,8 +1,8 @@
1
  package services
2
 
3
  import (
 
4
  "api.qobiltu.id/utils"
5
- "api.qobiltu.id/worker"
6
  "context"
7
  "github.com/hibiken/asynq"
8
  "strconv"
 
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"
space/space/space/space/.gitignore CHANGED
@@ -7,3 +7,4 @@ README.md
7
  logs/
8
  .idea
9
  my-notes
 
 
7
  logs/
8
  .idea
9
  my-notes
10
+ uploads
space/space/space/space/pkg/validation/custom_rules.go ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package validation
2
+
3
+ import (
4
+ "strings"
5
+ "sync"
6
+
7
+ v10 "github.com/go-playground/validator/v10"
8
+ "gorm.io/gorm"
9
+ )
10
+
11
+ type ValidOptionSource interface {
12
+ GetValidOptions(key string) ([]string, error)
13
+ GetValidKeys() []string
14
+ }
15
+
16
+ // --------------------
17
+ // InMemoryOptionSource
18
+ // --------------------
19
+
20
+ type InMemoryOptionSource struct{}
21
+
22
+ var inMemoryOptions = map[string][]string{
23
+ "last_education": {"SD", "SMP", "SMA", "D1", "D2", "D3", "D4", "D5", "S1", "S2", "S3"},
24
+ "marital_status": {"Belum Menikah", "Duda", "Janda"},
25
+ "gender": {"Laki-laki", "Perempuan"},
26
+ "monthly_expenses": {"< 2 Juta", "2-5 Juta", "5-20 Juta", "> 10 Juta"},
27
+ "monthly_income": {"< 3 Juta", "3-5 Juta", "5-10 Juta", "> 10 Juta"},
28
+ "religion": {"Islam", "Non-Islam"},
29
+ "family_role": {"Ayah", "Ibu", "Kakak", "Adik", "Anak"},
30
+ "life_status": {"Hidup", "Wafat"},
31
+ "body_shape": {"Ideal", "Kurus", "Berisi", "Gemuk"},
32
+ "skin_color": {"Putih", "Kuning Langsat", "Sawo Matang"},
33
+ "hair_type": {"Lurus", "Bergelombang", "Keriting"},
34
+ "frequently": {"Selalu", "Sering", "Kadang", "Jarang"},
35
+ "quran_reading_ability": {"Lancar", "Menengah", "Perlu Bimbingan"},
36
+ }
37
+
38
+ func (s *InMemoryOptionSource) GetValidOptions(key string) ([]string, error) {
39
+ return inMemoryOptions[key], nil
40
+ }
41
+
42
+ func (s *InMemoryOptionSource) GetValidKeys() []string {
43
+ keys := make([]string, 0, len(inMemoryOptions))
44
+ for k := range inMemoryOptions {
45
+ keys = append(keys, k)
46
+ }
47
+ return keys
48
+ }
49
+
50
+ // --------------------
51
+ // DBOptionSource
52
+ // --------------------
53
+
54
+ type DBOptionSource struct {
55
+ options map[string][]string
56
+ mu sync.RWMutex
57
+ }
58
+
59
+ type (
60
+ OptionCategory struct {
61
+ ID int64 `gorm:"primaryKey" json:"id"`
62
+ OptionName string `json:"option_name"`
63
+ OptionSlug string `json:"option_slug" gorm:"uniqueIndex"`
64
+ }
65
+
66
+ OptionValues struct {
67
+ ID int64 `gorm:"primaryKey" json:"id"`
68
+ OptionCategoryID int64 `json:"option_category_id"`
69
+ OptionValue string `json:"option_value"`
70
+ }
71
+ )
72
+
73
+ func NewDBOptionSource(db *gorm.DB) (*DBOptionSource, error) {
74
+ var categories []OptionCategory
75
+ if err := db.Find(&categories).Error; err != nil {
76
+ return nil, err
77
+ }
78
+
79
+ options := make(map[string][]string)
80
+ for _, cat := range categories {
81
+ var values []OptionValues
82
+ if err := db.Where("option_category_id = ?", cat.ID).Find(&values).Error; err != nil {
83
+ return nil, err
84
+ }
85
+ for _, val := range values {
86
+ options[cat.OptionSlug] = append(options[cat.OptionSlug], val.OptionValue)
87
+ }
88
+ }
89
+
90
+ return &DBOptionSource{options: options}, nil
91
+ }
92
+
93
+ func (s *DBOptionSource) GetValidOptions(key string) ([]string, error) {
94
+ s.mu.RLock()
95
+ defer s.mu.RUnlock()
96
+ return s.options[key], nil
97
+ }
98
+
99
+ func (s *DBOptionSource) GetValidKeys() []string {
100
+ s.mu.RLock()
101
+ defer s.mu.RUnlock()
102
+ keys := make([]string, 0, len(s.options))
103
+ for k := range s.options {
104
+ keys = append(keys, k)
105
+ }
106
+ return keys
107
+ }
108
+
109
+ // --------------------
110
+ // Validator
111
+ // --------------------
112
+
113
+ type Validator struct {
114
+ source ValidOptionSource
115
+ }
116
+
117
+ func NewValidatorRules(source ValidOptionSource) *Validator {
118
+ return &Validator{source: source}
119
+ }
120
+
121
+ func (v *Validator) GenericOptionRule(key string) func(fl v10.FieldLevel) bool {
122
+ return func(fl v10.FieldLevel) bool {
123
+ value := fl.Field().String()
124
+ if value == "" {
125
+ return true
126
+ }
127
+ validOptions, err := v.source.GetValidOptions(key)
128
+ if err != nil {
129
+ return false
130
+ }
131
+ for _, opt := range validOptions {
132
+ if opt == value {
133
+ return true
134
+ }
135
+ }
136
+ return false
137
+ }
138
+ }
139
+
140
+ func (v *Validator) RegisterAllCustomRules(validate *v10.Validate) error {
141
+ for _, key := range v.source.GetValidKeys() {
142
+ err := validate.RegisterValidation(key, v.GenericOptionRule(key))
143
+ if err != nil {
144
+ return err
145
+ }
146
+ }
147
+
148
+ err := validate.RegisterValidation("password", v.PasswordRule)
149
+ if err != nil {
150
+ return err
151
+ }
152
+
153
+ return nil
154
+ }
155
+
156
+ func (v *Validator) PasswordRule(fl v10.FieldLevel) bool {
157
+ password := fl.Field().String()
158
+ return len(password) >= 8 &&
159
+ strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") &&
160
+ strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") &&
161
+ strings.ContainsAny(password, "0123456789")
162
+ }
space/space/space/space/pkg/validation/validation.go ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package validation
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "strings"
7
+
8
+ "github.com/go-playground/locales/en"
9
+ "github.com/go-playground/locales/id"
10
+ ut "github.com/go-playground/universal-translator"
11
+ v10 "github.com/go-playground/validator/v10"
12
+ entranslations "github.com/go-playground/validator/v10/translations/en"
13
+ idtranslations "github.com/go-playground/validator/v10/translations/id"
14
+ )
15
+
16
+ // Constants for supported locales
17
+ const (
18
+ LocaleID = "id"
19
+ LocaleEN = "en"
20
+ )
21
+
22
+ // ErrorMessage represents a validation error message
23
+ type ErrorMessage struct {
24
+ Field string `json:"field"`
25
+ Message string `json:"-"`
26
+ }
27
+
28
+ type validator struct {
29
+ validate *v10.Validate
30
+ translator ut.Translator
31
+ }
32
+
33
+ // validatorInstance adalah instance global dari validator.
34
+ var validatorInstance *validator
35
+
36
+ // New creates a new validation instance with the specified locale
37
+ // dan menginisialisasi instance global validatorInstance.
38
+ func New(locale string) error {
39
+ v := &validator{}
40
+ parsedLocale := parseLocale(locale)
41
+
42
+ uni := ut.New(en.New(), id.New(), en.New())
43
+ translator, found := uni.GetTranslator(parsedLocale)
44
+ if !found {
45
+ return fmt.Errorf("translator not found for locale: %s", parsedLocale)
46
+ }
47
+
48
+ validate := v10.New()
49
+
50
+ if err := setupValidations(validate); err != nil {
51
+ return fmt.Errorf("failed to setup validations: %w", err)
52
+ }
53
+
54
+ if err := setupTranslations(validate, translator, parsedLocale); err != nil {
55
+ return fmt.Errorf("failed to setup translations for locale %s: %w", parsedLocale, err)
56
+ }
57
+
58
+ v.validate = validate
59
+ v.translator = translator
60
+
61
+ validatorInstance = v // Inisialisasi instance global
62
+ return nil
63
+ }
64
+
65
+ func parseLocale(locale string) string {
66
+ switch strings.ToLower(locale) {
67
+ case "id":
68
+ return LocaleID
69
+ case "en":
70
+ return LocaleEN
71
+ default:
72
+ return LocaleID // Default to Indonesian
73
+ }
74
+ }
75
+
76
+ // setupValidations configures custom validation rules.
77
+ func setupValidations(validate *v10.Validate) error {
78
+ rules := NewValidatorRules(&InMemoryOptionSource{})
79
+ if err := rules.RegisterAllCustomRules(validate); err != nil {
80
+ return err
81
+ }
82
+ return nil
83
+ }
84
+
85
+ // setupTranslations configures translations for validation messages.
86
+ func setupTranslations(validate *v10.Validate, translator ut.Translator, locale string) error {
87
+ // Register default translations based on locale
88
+ if err := registerDefaultTranslations(validate, translator, locale); err != nil {
89
+ return fmt.Errorf("failed to register default translations for locale %s: %w", locale, err)
90
+ }
91
+
92
+ // Register custom password validation translation
93
+ err := validate.RegisterTranslation("password", translator,
94
+ func(ut ut.Translator) error {
95
+ return ut.Add("password", "harus mengandung minimal 8 karakter, huruf besar, huruf kecil, dan angka.", true)
96
+ },
97
+ func(ut ut.Translator, fe v10.FieldError) string {
98
+ translated, err := ut.T(fe.Tag(), fe.Field())
99
+ if err != nil {
100
+ return fe.Field() + " is invalid"
101
+ }
102
+ return translated
103
+ },
104
+ )
105
+ if err != nil {
106
+ return fmt.Errorf("failed to register password translation: %w", err)
107
+ }
108
+
109
+ return nil
110
+ }
111
+
112
+ // registerDefaultTranslations sets up default translations for the specified locale.
113
+ func registerDefaultTranslations(validate *v10.Validate, translator ut.Translator, locale string) error {
114
+ switch locale {
115
+ case LocaleID:
116
+ return idtranslations.RegisterDefaultTranslations(validate, translator)
117
+ case LocaleEN:
118
+ return entranslations.RegisterDefaultTranslations(validate, translator)
119
+ default:
120
+ // Fallback to English if the locale is not supported
121
+ return entranslations.RegisterDefaultTranslations(validate, translator)
122
+ }
123
+ }
124
+
125
+ // Validate validates a struct using the global validator instance
126
+ // and returns a slice of ErrorMessage.
127
+ func Validate(s any) []ErrorMessage {
128
+ if validatorInstance == nil {
129
+ return []ErrorMessage{{Field: "", Message: "Validator belum diinisialisasi. Panggil validation.New() terlebih dahulu."}}
130
+ }
131
+ err := validatorInstance.validate.Struct(s)
132
+ if err != nil {
133
+ return TranslateError(err)
134
+ }
135
+ return nil
136
+ }
137
+
138
+ // TranslateError takes a validation error and translates it using the global translator.
139
+ func TranslateError(err error) []ErrorMessage {
140
+ if validatorInstance == nil {
141
+ return nil
142
+ }
143
+
144
+ var validationErrors v10.ValidationErrors
145
+ if !errors.As(err, &validationErrors) {
146
+ return nil
147
+ }
148
+
149
+ errorMessages := make([]ErrorMessage, 0, len(validationErrors))
150
+ for _, e := range validationErrors {
151
+ fieldLabel := e.Field()
152
+ msg, err := validatorInstance.translator.T(e.Tag(), fieldLabel)
153
+ if err != nil {
154
+ msg = fieldLabel + " is Invalid"
155
+ }
156
+
157
+ errorMessages = append(errorMessages, ErrorMessage{
158
+ Field: e.Tag(),
159
+ Message: msg,
160
+ })
161
+ }
162
+
163
+ return errorMessages
164
+ }
space/space/space/space/pkg/worker/distributor.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "context"
5
+ "github.com/hibiken/asynq"
6
+ )
7
+
8
+ type TaskDistributor interface {
9
+ DistributeTaskSendVerifyEmail(
10
+ ctx context.Context,
11
+ payload *PayloadSendVerifyEmail,
12
+ opts ...asynq.Option,
13
+ ) error
14
+
15
+ DistributeTaskSendForgotPasswordEmail(
16
+ ctx context.Context,
17
+ payload *PayloadSendForgotPasswordEmail,
18
+ opts ...asynq.Option,
19
+ ) error
20
+ }
21
+
22
+ // AsyncTaskDistributor is a global variable to hold the task distributor
23
+ var AsyncTaskDistributor TaskDistributor
space/space/space/space/pkg/worker/logger.go ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log/slog"
7
+ )
8
+
9
+ type Logger struct{}
10
+
11
+ func NewLogger() *Logger {
12
+ return &Logger{}
13
+ }
14
+
15
+ func (logger *Logger) Printf(ctx context.Context, format string, v ...interface{}) {
16
+ slog.Info(fmt.Sprintf(format, v...))
17
+ }
18
+
19
+ func (logger *Logger) Debug(args ...interface{}) {
20
+ slog.Debug(fmt.Sprint(args...))
21
+ }
22
+
23
+ func (logger *Logger) Info(args ...interface{}) {
24
+ slog.Info(fmt.Sprint(args...))
25
+ }
26
+
27
+ func (logger *Logger) Warn(args ...interface{}) {
28
+ slog.Warn(fmt.Sprint(args...))
29
+ }
30
+
31
+ func (logger *Logger) Error(args ...interface{}) {
32
+ slog.Error(fmt.Sprint(args...))
33
+ }
34
+
35
+ func (logger *Logger) Fatal(args ...interface{}) {
36
+ slog.Error(fmt.Sprint(args...))
37
+ }
space/space/space/space/pkg/worker/processor.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "api.qobiltu.id/mail"
5
+ "context"
6
+ "github.com/hibiken/asynq"
7
+ "github.com/redis/go-redis/v9"
8
+ "log/slog"
9
+ )
10
+
11
+ const (
12
+ Low = "low"
13
+ Default = "default"
14
+ Critical = "critical"
15
+ )
16
+
17
+ type TaskProcessor interface {
18
+ Start() error
19
+ Shutdown()
20
+ ProcessTaskSendVerifyEmail(ctx context.Context, task *asynq.Task) error
21
+ ProcessTaskSendForgotPasswordEmail(ctx context.Context, task *asynq.Task) error
22
+ }
23
+
24
+ type RedisTaskProcessor struct {
25
+ server *asynq.Server
26
+ emailSender mail.Sender
27
+ }
28
+
29
+ func NewRedisTaskProcessor(redisOpt asynq.RedisClientOpt, emailSender mail.Sender) TaskProcessor {
30
+ logger := NewLogger()
31
+ redis.SetLogger(logger)
32
+
33
+ server := asynq.NewServer(
34
+ redisOpt,
35
+ asynq.Config{
36
+ // priority value. Keys are the names of the queues and values are associated priority value.
37
+ Queues: map[string]int{
38
+ Critical: 6,
39
+ Default: 3,
40
+ Low: 1,
41
+ },
42
+ ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
43
+ slog.Error("process task failed", "error", err, "type", task.Type(), "payload", string(task.Payload()))
44
+ }),
45
+ // maximum number of concurrent processing of tasks.
46
+ Concurrency: 50,
47
+ Logger: logger,
48
+ },
49
+ )
50
+
51
+ return &RedisTaskProcessor{
52
+ server: server,
53
+ emailSender: emailSender,
54
+ }
55
+ }
56
+
57
+ func (p *RedisTaskProcessor) Start() error {
58
+ mux := asynq.NewServeMux()
59
+ mux.HandleFunc(TaskSendVerifyEmail, p.ProcessTaskSendVerifyEmail)
60
+ mux.HandleFunc(TaskSendForgotPasswordEmail, p.ProcessTaskSendForgotPasswordEmail)
61
+ return p.server.Start(mux)
62
+ }
63
+
64
+ func (p *RedisTaskProcessor) Shutdown() {
65
+ p.server.Shutdown()
66
+ }
space/space/space/space/pkg/worker/redis_distributor.go ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "github.com/hibiken/asynq"
5
+ )
6
+
7
+ type RedisTaskDistributor struct {
8
+ client *asynq.Client
9
+ }
10
+
11
+ func NewRedisTaskDistributor(redisOpt asynq.RedisClientOpt) TaskDistributor {
12
+ client := asynq.NewClient(redisOpt)
13
+ return &RedisTaskDistributor{
14
+ client: client,
15
+ }
16
+ }
space/space/space/space/pkg/worker/task_send_forgot_password_email.go ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "api.qobiltu.id/assets"
5
+ "bytes"
6
+ "context"
7
+ "encoding/json"
8
+ "fmt"
9
+ "github.com/hibiken/asynq"
10
+ "html/template"
11
+ "log/slog"
12
+ )
13
+
14
+ const (
15
+ TaskSendForgotPasswordEmailMaxRetry = 5
16
+ TaskSendForgotPasswordEmail = "task:send_forgot_password_email"
17
+ TaskSendForgotPasswordEmailSubject = "Permintaan Reset Password"
18
+ )
19
+
20
+ type PayloadSendForgotPasswordEmail struct {
21
+ EmailAddress string `json:"email_address"`
22
+ ResetToken string `json:"reset_token"`
23
+ ExpirationInMinutes int `json:"expiration_in_minutes"`
24
+ Subject string `json:"subject"`
25
+ AppName string `json:"app_name"`
26
+ }
27
+
28
+ func (d *RedisTaskDistributor) DistributeTaskSendForgotPasswordEmail(
29
+ ctx context.Context,
30
+ payload *PayloadSendForgotPasswordEmail,
31
+ opts ...asynq.Option,
32
+ ) error {
33
+ jsonPayload, err := json.Marshal(payload)
34
+ if err != nil {
35
+ return fmt.Errorf("failed to marshal task payload: %w", err)
36
+ }
37
+
38
+ task := asynq.NewTask(TaskSendForgotPasswordEmail, jsonPayload, opts...)
39
+
40
+ _, err = d.client.EnqueueContext(ctx, task)
41
+ if err != nil {
42
+ return fmt.Errorf("failed to enqueue task: %w", err)
43
+ }
44
+
45
+ return nil
46
+ }
47
+
48
+ func (p *RedisTaskProcessor) ProcessTaskSendForgotPasswordEmail(ctx context.Context, task *asynq.Task) error {
49
+ var payload PayloadSendForgotPasswordEmail
50
+ if err := json.Unmarshal(task.Payload(), &payload); err != nil {
51
+ return fmt.Errorf("failed to unmarshal payload: %w", asynq.SkipRetry)
52
+ }
53
+
54
+ var tmpl *template.Template
55
+ tmpl, err := template.ParseFS(assets.EmbeddedFiles, assets.EmailForgotPasswordTemplatePath)
56
+ if err != nil {
57
+ return fmt.Errorf("failed to parse forgot password email template: %w", err)
58
+ }
59
+ var body bytes.Buffer
60
+ if err := tmpl.ExecuteTemplate(&body, "htmlBody", payload); err != nil {
61
+ return fmt.Errorf("failed to execute forgot password email template: %w", err)
62
+ }
63
+ htmlContent := body.String()
64
+
65
+ slog.Info("Sending forgot password email", slog.String("email", payload.EmailAddress))
66
+
67
+ err = p.emailSender.Send(payload.EmailAddress, payload.Subject, htmlContent, payload)
68
+ if err != nil {
69
+ return fmt.Errorf("failed to send forgot password email: %w", err)
70
+ }
71
+
72
+ slog.Info("Forgot password email sent successfully", slog.String("email", payload.EmailAddress))
73
+
74
+ return nil
75
+ }
space/space/space/space/pkg/worker/task_send_verify_email.go ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package worker
2
+
3
+ import (
4
+ "api.qobiltu.id/assets"
5
+ "bytes"
6
+ "context"
7
+ "encoding/json"
8
+ "fmt"
9
+ "github.com/hibiken/asynq"
10
+ "html/template"
11
+ "log/slog"
12
+ )
13
+
14
+ const (
15
+ TaskSendVerifyEmailMaxRetry = 3
16
+ TaskSendVerifyEmail = "task:send_verify_email"
17
+ TaskSendVerifyEmailSubject = "Verifikasi Email"
18
+ )
19
+
20
+ type PayloadSendVerifyEmail struct {
21
+ EmailAddress string `json:"email_address"`
22
+ VerificationCode string `json:"verification_code"`
23
+ ExpirationInMinutes int `json:"expiration_in_minutes"`
24
+ Subject string `json:"subject"`
25
+ }
26
+
27
+ func (d *RedisTaskDistributor) DistributeTaskSendVerifyEmail(
28
+ ctx context.Context,
29
+ payload *PayloadSendVerifyEmail,
30
+ opts ...asynq.Option,
31
+ ) error {
32
+ jsonPayload, err := json.Marshal(payload)
33
+ if err != nil {
34
+ return fmt.Errorf("failed to marshal task payload: %w", err)
35
+ }
36
+
37
+ task := asynq.NewTask(TaskSendVerifyEmail, jsonPayload, opts...)
38
+
39
+ _, err = d.client.EnqueueContext(ctx, task)
40
+ if err != nil {
41
+ return fmt.Errorf("failed to enqueue task: %w", err)
42
+ }
43
+
44
+ return nil
45
+ }
46
+
47
+ func (p *RedisTaskProcessor) ProcessTaskSendVerifyEmail(ctx context.Context, task *asynq.Task) error {
48
+ var payload PayloadSendVerifyEmail
49
+ if err := json.Unmarshal(task.Payload(), &payload); err != nil {
50
+ return fmt.Errorf("failed to unmarshal payload: %w", asynq.SkipRetry)
51
+ }
52
+
53
+ var tmpl *template.Template
54
+ tmpl, err := template.ParseFS(assets.EmbeddedFiles, assets.EmailConfirmationTemplatePath)
55
+ if err != nil {
56
+ return fmt.Errorf("failed to parse email template: %w", err)
57
+ }
58
+
59
+ var body bytes.Buffer
60
+ if err := tmpl.ExecuteTemplate(&body, "htmlBody", payload); err != nil {
61
+ return fmt.Errorf("failed to execute email template: %w", err)
62
+ }
63
+ htmlContent := body.String()
64
+
65
+ slog.Info("Sending verification email", slog.String("email", payload.EmailAddress))
66
+
67
+ err = p.emailSender.Send(payload.EmailAddress, payload.Subject, htmlContent, payload)
68
+ if err != nil {
69
+ return fmt.Errorf("failed to send verify email: %w", err)
70
+ }
71
+
72
+ slog.Info("Verification email sent successfully", slog.String("email", payload.EmailAddress))
73
+
74
+ return nil
75
+ }
space/space/space/space/space/main.go CHANGED
@@ -9,6 +9,7 @@ import (
9
  "api.qobiltu.id/router"
10
  "api.qobiltu.id/services"
11
  "api.qobiltu.id/utils"
 
12
  "api.qobiltu.id/worker"
13
  "github.com/hibiken/asynq"
14
  "log/slog"
@@ -18,6 +19,10 @@ import (
18
 
19
  func main() {
20
 
 
 
 
 
21
  // setup email sender
22
  emailConfig := mail.Config{
23
  Host: config.SMTP_HOST,
 
9
  "api.qobiltu.id/router"
10
  "api.qobiltu.id/services"
11
  "api.qobiltu.id/utils"
12
+ "api.qobiltu.id/validation"
13
  "api.qobiltu.id/worker"
14
  "github.com/hibiken/asynq"
15
  "log/slog"
 
19
 
20
  func main() {
21
 
22
+ // setup validation
23
+ err := validation.New(validation.LocaleID)
24
+ utils.FatalIfErr("failed to setup validator", err)
25
+
26
  // setup email sender
27
  emailConfig := mail.Config{
28
  Host: config.SMTP_HOST,
space/space/space/space/space/models/exception_model.go CHANGED
@@ -1,23 +1,26 @@
1
  package models
2
 
 
 
3
  type Exception struct {
4
- Unauthorized bool `json:"unauthorized,omitempty"`
5
- BadRequest bool `json:"bad_request,omitempty"`
6
- DataNotFound bool `json:"data_not_found,omitempty"`
7
- InternalServerError bool `json:"internal_server_error,omitempty"`
8
- DataDuplicate bool `json:"data_duplicate,omitempty"`
9
- QueryError bool `json:"query_error,omitempty"`
10
- InvalidPasswordLength bool `json:"invalid_password_length,omitempty"`
11
- IsPassTheLimit bool `json:"is_pass_the_limit,omitempty"`
12
- IsTimeOut bool `json:"is_time_out,omitempty"`
13
- AttemptNotFound bool `json:"attempt_not_found,omitempty"`
14
- Forbidden bool `json:"forbidden,omitempty"`
15
- ValidationError bool `json:"validation_error,omitempty"`
16
 
17
- Message string `json:"message,omitempty"`
18
- Err error `json:"-"`
 
19
  }
20
 
21
  func (a Exception) Error() string {
22
- return a.Err.Error()
23
  }
 
1
  package models
2
 
3
+ import "api.qobiltu.id/validation"
4
+
5
  type Exception struct {
6
+ Unauthorized bool `json:"unauthorized,omitempty"`
7
+ BadRequest bool `json:"bad_request,omitempty"`
8
+ DataNotFound bool `json:"data_not_found,omitempty"`
9
+ InternalServerError bool `json:"internal_server_error,omitempty"`
10
+ DataDuplicate bool `json:"data_duplicate,omitempty"`
11
+ QueryError bool `json:"query_error,omitempty"`
12
+ InvalidPasswordLength bool `json:"invalid_password_length,omitempty"`
13
+ IsPassTheLimit bool `json:"is_pass_the_limit,omitempty"`
14
+ IsTimeOut bool `json:"is_time_out,omitempty"`
15
+ AttemptNotFound bool `json:"attempt_not_found,omitempty"`
16
+ Forbidden bool `json:"forbidden,omitempty"`
17
+ ValidationError bool `json:"validation_error,omitempty"`
18
 
19
+ Message string `json:"message,omitempty"`
20
+ Err error `json:"-"`
21
+ ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
22
  }
23
 
24
  func (a Exception) Error() string {
25
+ return a.Err.Error()
26
  }
space/space/space/space/space/response/api_response_v2.go CHANGED
@@ -3,6 +3,7 @@ package response
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/utils"
 
6
  "errors"
7
  "net/http"
8
 
@@ -39,7 +40,7 @@ func HandleError(c *gin.Context, err error) {
39
  case exception.AttemptNotFound:
40
  responseError(c, http.StatusNotFound, exception)
41
  case exception.ValidationError:
42
- responseError(c, http.StatusUnprocessableEntity, exception)
43
  default:
44
  responseError(c, http.StatusInternalServerError, exception)
45
  }
@@ -77,3 +78,17 @@ func responseError(c *gin.Context, status int, exception models.Exception) {
77
  c.AbortWithStatusJSON(status, res)
78
  return
79
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/utils"
6
+ "api.qobiltu.id/validation"
7
  "errors"
8
  "net/http"
9
 
 
40
  case exception.AttemptNotFound:
41
  responseError(c, http.StatusNotFound, exception)
42
  case exception.ValidationError:
43
+ responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields) // Gunakan fungsi khusus untuk validasi
44
  default:
45
  responseError(c, http.StatusInternalServerError, exception)
46
  }
 
78
  c.AbortWithStatusJSON(status, res)
79
  return
80
  }
81
+
82
+ func responseValidationError(c *gin.Context, status int, validationErrors []validation.ErrorMessage) {
83
+ res := models.ErrorResponse{
84
+ Status: "error",
85
+ Message: "Validasi data gagal.",
86
+ Errors: models.Exception{
87
+ ValidationError: true,
88
+ ValidationErrorFields: validationErrors,
89
+ },
90
+ }
91
+
92
+ c.AbortWithStatusJSON(status, res)
93
+ return
94
+ }
space/space/space/space/space/response/validation.go ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package response
2
+
3
+ import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/validation"
6
+ )
7
+
8
+ func HandleValidationError(validationErrors []validation.ErrorMessage) error {
9
+ return models.Exception{
10
+ ValidationError: true,
11
+ Message: "Validation failed",
12
+ ValidationErrorFields: validationErrors,
13
+ }
14
+ }
space/space/space/space/space/services/cv_service.go CHANGED
@@ -1,444 +1,485 @@
1
  package services
2
 
3
  import (
4
- "api.qobiltu.id/models"
5
- "api.qobiltu.id/repositories"
6
- "api.qobiltu.id/response"
7
- "context"
8
- "errors"
9
- "gorm.io/gorm"
 
10
  )
11
 
12
  type CVService interface {
13
- SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error)
14
- GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error)
15
-
16
- SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error)
17
- GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error)
18
-
19
- CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
20
- UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
21
- ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
22
- GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
23
- DeleteFamilyMember(ctx context.Context, id int64) error
24
-
25
- SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error)
26
- GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error)
27
-
28
- SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error)
29
- GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error)
30
-
31
- CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error)
32
- UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error)
33
- ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
34
- GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
35
- DeleteEducation(ctx context.Context, id int64) error
36
-
37
- CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error)
38
- UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error)
39
- ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
40
- GetJob(ctx context.Context, id int64) (*models.JobCV, error)
41
- DeleteJob(ctx context.Context, id int64) error
42
-
43
- CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error)
44
- UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error)
45
- ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
46
- GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
47
- DeleteAchievement(ctx context.Context, id int64) error
48
  }
49
 
50
  type cvService struct {
51
- cvRepository repositories.CVRepository
52
  }
53
 
54
  func NewCVService(cvRepository repositories.CVRepository) CVService {
55
- return &cvService{
56
- cvRepository: cvRepository,
57
- }
58
  }
59
 
60
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error) {
61
- // Ambil data lama jika ada
62
- accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, req.AccountID)
63
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
64
- return nil, response.HandleGormError(err, "Internal Server Error")
65
- }
66
-
67
- // Apply perubahan
68
- if accountDetails == nil {
69
- accountDetails = &models.AccountDetails{}
70
- }
71
-
72
- accountDetails.AccountID = uint(req.AccountID)
73
- accountDetails.FullName = req.FullName
74
- accountDetails.Gender = req.Gender
75
- accountDetails.DateOfBirth = req.DateOfBirth
76
- accountDetails.PlaceOfBirth = req.PlaceOfBirth
77
- accountDetails.Domicile = req.Domicile
78
- accountDetails.MaritalStatus = req.MaritalStatus
79
- accountDetails.LastEducation = req.LastEducation
80
- accountDetails.LastJob = req.LastJob
81
-
82
- // Simpan data
83
- res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
84
- if err != nil {
85
- return nil, response.HandleGormError(err, "Internal Server Error")
86
- }
87
-
88
- return res, nil
 
 
 
 
89
  }
90
 
91
  func (s *cvService) GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error) {
92
- res, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, id)
93
- if err != nil {
94
- return nil, response.HandleGormError(err, "Data diri tidak ditemukan")
95
- }
96
- return res, nil
97
  }
98
 
99
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error) {
100
- // Ambil data lama jika ada
101
- personalityAndPreference, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
102
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
103
- return nil, response.HandleGormError(err, "Internal Server Error")
104
- }
105
-
106
- // Apply perubahan
107
- if personalityAndPreference == nil {
108
- personalityAndPreference = &models.PersonalityAndPreferenceCV{}
109
- }
110
-
111
- personalityAndPreference.AccountID = req.AccountID
112
- personalityAndPreference.PositiveTraits = req.PositiveTraits
113
- personalityAndPreference.NegativeTraits = req.NegativeTraits
114
- personalityAndPreference.Hobbies = req.Hobbies
115
- personalityAndPreference.LifeGoals = req.LifeGoals
116
- personalityAndPreference.DailyActivities = req.DailyActivities
117
- personalityAndPreference.LeisureActivities = req.LeisureActivities
118
- personalityAndPreference.Likes = req.Likes
119
- personalityAndPreference.Dislikes = req.Dislikes
120
- personalityAndPreference.StressHandling = req.StressHandling
121
- personalityAndPreference.AngerTriggers = req.AngerTriggers
122
- personalityAndPreference.FavoriteFoodAndDrinks = req.FavoriteFoodAndDrinks
123
- personalityAndPreference.CanCook = req.CanCook
124
- personalityAndPreference.TypesOfDishesCooked = req.TypesOfDishesCooked
125
- personalityAndPreference.MonthlyExpenses = req.MonthlyExpenses
126
-
127
- res, err := s.cvRepository.SavePersonalityAndPreference(ctx, personalityAndPreference)
128
- if err != nil {
129
- return nil, response.HandleGormError(err, "Internal Server Error")
130
- }
131
-
132
- return res, nil
 
 
 
 
133
  }
134
 
135
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error) {
136
- res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, id)
137
- if err != nil {
138
- return nil, response.HandleGormError(err, "Internal Server Error")
139
- }
140
 
141
- return res, nil
142
  }
143
 
144
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
145
- // Mapping request ke model
146
- familyMember := &models.FamilyMemberCV{
147
- AccountID: req.AccountID,
148
- Role: req.Role,
149
- Status: req.Status,
150
- Religion: req.Religion,
151
- Job: req.Job,
152
- LastEducation: req.LastEducation,
153
- Age: req.Age,
154
- }
155
-
156
- // Simpan ke repository
157
- res, err := s.cvRepository.SaveFamilyMember(ctx, familyMember)
158
- if err != nil {
159
- return nil, response.HandleGormError(err, "Gagal menyimpan anggota keluarga")
160
- }
161
-
162
- return res, nil
 
 
 
 
163
  }
164
 
165
  func (s *cvService) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
166
- list, err := s.cvRepository.ListFamilyMember(ctx, accountID)
167
- if err != nil {
168
- return nil, response.HandleGormError(err, "Gagal mengambil daftar anggota keluarga")
169
- }
170
- return list, nil
171
  }
172
 
173
  func (s *cvService) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
174
- res, err := s.cvRepository.GetFamilyMember(ctx, id)
175
- if err != nil {
176
- return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
177
- }
178
- return res, nil
179
  }
180
 
181
  func (s *cvService) DeleteFamilyMember(ctx context.Context, id int64) error {
182
- err := s.cvRepository.DeleteFamilyMember(ctx, id)
183
- if err != nil {
184
- return response.HandleGormError(err, "Gagal menghapus anggota keluarga")
185
- }
186
- return nil
187
  }
188
 
189
  func (s *cvService) UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
190
- existing, err := s.cvRepository.GetFamilyMember(ctx, id)
191
- if err != nil {
192
- return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
193
- }
194
-
195
- existing.Role = req.Role
196
- existing.Status = req.Status
197
- existing.Religion = req.Religion
198
- existing.Job = req.Job
199
- existing.LastEducation = req.LastEducation
200
- existing.Age = req.Age
201
-
202
- updated, err := s.cvRepository.SaveFamilyMember(ctx, existing)
203
- if err != nil {
204
- return nil, response.HandleGormError(err, "Gagal memperbarui anggota keluarga")
205
- }
206
-
207
- return updated, nil
 
 
 
 
208
  }
209
 
210
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
211
- // Cek apakah data sudah ada berdasarkan account_id
212
- existing, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
213
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
214
- return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data fisik dan kesehatan")
215
- }
216
-
217
- // Jika belum ada, buat objek baru
218
- if existing == nil {
219
- existing = &models.PhysicalAndHealthCV{}
220
- }
221
-
222
- // Mapping field dari request
223
- existing.AccountID = req.AccountID
224
- existing.HeightInCm = req.HeightInCm
225
- existing.WeightInKg = req.WeightInKg
226
- existing.BodyShape = req.BodyShape
227
- existing.SkinColor = req.SkinColor
228
- existing.HairType = req.HairType
229
- existing.MedicalHistory = req.MedicalHistory
230
- existing.PhysicalDisorder = req.PhysicalDisorder
231
- existing.PhysicalTraits = req.PhysicalTraits
232
-
233
- // Simpan data
234
- res, err := s.cvRepository.SavePhysicalAndHealth(ctx, existing)
235
- if err != nil {
236
- return nil, response.HandleGormError(err, "Gagal menyimpan data fisik dan kesehatan")
237
- }
238
-
239
- return res, nil
 
 
 
 
240
  }
241
 
242
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error) {
243
- res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, id)
244
- if err != nil {
245
- return nil, response.HandleGormError(err, "Data fisik dan kesehatan tidak ditemukan")
246
- }
247
- return res, nil
248
  }
249
 
250
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
251
- // Cek apakah data sudah ada berdasarkan account_id
252
- worshipAndReligiousUnderstanding, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
253
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
254
- return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data agama dan pemahaman agama")
255
- }
256
-
257
- // Jika belum ada, buat objek baru
258
- if worshipAndReligiousUnderstanding == nil {
259
- worshipAndReligiousUnderstanding = &models.WorshipAndReligiousUnderstandingCV{}
260
- }
261
-
262
- // Mapping field dari request
263
- worshipAndReligiousUnderstanding.AccountID = req.AccountID
264
- worshipAndReligiousUnderstanding.ObligatoryPrayer = req.ObligatoryPrayer
265
- worshipAndReligiousUnderstanding.CongregationalPrayer = req.CongregationalPrayer
266
- worshipAndReligiousUnderstanding.TahajjudPrayer = req.TahajjudPrayer
267
- worshipAndReligiousUnderstanding.DhuhaPrayer = req.DhuhaPrayer
268
- worshipAndReligiousUnderstanding.QuranMemorization = req.QuranMemorization
269
- worshipAndReligiousUnderstanding.QuranReadingAbility = req.QuranReadingAbility
270
- worshipAndReligiousUnderstanding.DaudFasting = req.DaudFasting
271
- worshipAndReligiousUnderstanding.AyyamulBidhFasting = req.AyyamulBidhFasting
272
- worshipAndReligiousUnderstanding.HajjOrUmrah = req.HajjOrUmrah
273
- worshipAndReligiousUnderstanding.ListeningToMusic = req.ListeningToMusic
274
- worshipAndReligiousUnderstanding.OpinionOnIkhtilat = req.OpinionOnIkhtilat
275
- worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram = req.OpinionOnTouchingNonMahram
276
- worshipAndReligiousUnderstanding.OpinionOnVeil = req.OpinionOnVeil
277
- worshipAndReligiousUnderstanding.WeeklyReligiousStudies = req.WeeklyReligiousStudies
278
- worshipAndReligiousUnderstanding.FollowedUstadz = req.FollowedUstadz
279
-
280
- // Simpan data
281
- res, err := s.cvRepository.SaveWorshipAndReligiousUnderstanding(ctx, worshipAndReligiousUnderstanding)
282
- if err != nil {
283
- return nil, response.HandleGormError(err, "Internal Server Error")
284
- }
285
-
286
- return res, nil
 
 
 
 
287
  }
288
 
289
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
290
- res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, id)
291
- if err != nil {
292
- return nil, response.HandleGormError(err, "Data agama dan pemahaman agama tidak ditemukan")
293
- }
294
- return res, nil
295
  }
296
 
297
  func (s *cvService) CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error) {
298
- edu := &models.EducationCV{
299
- AccountID: req.AccountID,
300
- LastEducation: req.LastEducation,
301
- EducationInstitute: req.EducationInstitute,
302
- EducationMajor: req.EducationMajor,
303
- YearStart: req.YearStart,
304
- YearGraduate: req.YearGraduate,
305
- }
306
-
307
- res, err := s.cvRepository.SaveEducation(ctx, edu)
308
- if err != nil {
309
- return nil, response.HandleGormError(err, "Gagal menambahkan data pendidikan")
310
- }
311
-
312
- return res, nil
 
 
 
 
313
  }
314
 
315
  func (s *cvService) UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error) {
316
- edu, err := s.cvRepository.GetEducation(ctx, id)
317
- if err != nil {
318
- return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
319
- }
320
-
321
- edu.LastEducation = req.LastEducation
322
- edu.EducationInstitute = req.EducationInstitute
323
- edu.EducationMajor = req.EducationMajor
324
- edu.YearStart = req.YearStart
325
- edu.YearGraduate = req.YearGraduate
326
-
327
- res, err := s.cvRepository.SaveEducation(ctx, edu)
328
- if err != nil {
329
- return nil, response.HandleGormError(err, "Gagal memperbarui data pendidikan")
330
- }
331
- return res, nil
 
 
 
 
332
  }
333
 
334
  func (s *cvService) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
335
- return s.cvRepository.ListEducation(ctx, accountID)
336
  }
337
 
338
  func (s *cvService) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
339
- edu, err := s.cvRepository.GetEducation(ctx, id)
340
- if err != nil {
341
- return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
342
- }
343
- return edu, nil
344
  }
345
 
346
  func (s *cvService) DeleteEducation(ctx context.Context, id int64) error {
347
- return s.cvRepository.DeleteEducation(ctx, id)
348
  }
349
 
350
  func (s *cvService) CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error) {
351
- job := &models.JobCV{
352
- AccountID: req.AccountID,
353
- InstitutionName: req.InstitutionName,
354
- CurrentJob: req.CurrentJob,
355
- YearStartedWorking: req.YearStartedWorking,
356
- MonthlyIncome: req.MonthlyIncome,
357
- IncomeSources: req.IncomeSources,
358
- }
359
- res, err := s.cvRepository.SaveJob(ctx, job)
360
- if err != nil {
361
- return nil, response.HandleGormError(err, "Gagal menambahkan data pekerjaan")
362
- }
363
- return res, nil
 
 
 
 
364
  }
365
 
366
  func (s *cvService) UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error) {
367
- job, err := s.cvRepository.GetJob(ctx, id)
368
- if err != nil {
369
- return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
370
- }
371
-
372
- job.InstitutionName = req.InstitutionName
373
- job.CurrentJob = req.CurrentJob
374
- job.YearStartedWorking = req.YearStartedWorking
375
- job.MonthlyIncome = req.MonthlyIncome
376
- job.IncomeSources = req.IncomeSources
377
-
378
- res, err := s.cvRepository.SaveJob(ctx, job)
379
- if err != nil {
380
- return nil, response.HandleGormError(err, "Gagal memperbarui data pekerjaan")
381
- }
382
- return res, nil
 
 
 
 
383
  }
384
 
385
  func (s *cvService) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
386
- return s.cvRepository.ListJob(ctx, accountID)
387
  }
388
 
389
  func (s *cvService) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
390
- job, err := s.cvRepository.GetJob(ctx, id)
391
- if err != nil {
392
- return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
393
- }
394
- return job, nil
395
  }
396
 
397
  func (s *cvService) DeleteJob(ctx context.Context, id int64) error {
398
- return s.cvRepository.DeleteJob(ctx, id)
399
  }
400
 
401
  func (s *cvService) CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error) {
402
- ach := &models.AchievementCV{
403
- AccountID: req.AccountID,
404
- AchievementOrAward: req.AchievementOrAward,
405
- }
406
- res, err := s.cvRepository.SaveAchievement(ctx, ach)
407
- if err != nil {
408
- return nil, response.HandleGormError(err, "Gagal menambahkan data prestasi")
409
- }
410
-
411
- return res, nil
412
  }
413
 
414
  func (s *cvService) UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error) {
415
- ach, err := s.cvRepository.GetAchievement(ctx, id)
416
- if err != nil {
417
- return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
418
- }
419
 
420
- ach.AchievementOrAward = req.AchievementOrAward
421
 
422
- res, err := s.cvRepository.SaveAchievement(ctx, ach)
423
- if err != nil {
424
- return nil, response.HandleGormError(err, "Gagal memperbarui data prestasi")
425
- }
426
 
427
- return res, nil
428
  }
429
 
430
  func (s *cvService) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
431
- return s.cvRepository.ListAchievement(ctx, accountID)
432
  }
433
 
434
  func (s *cvService) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
435
- ach, err := s.cvRepository.GetAchievement(ctx, id)
436
- if err != nil {
437
- return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
438
- }
439
- return ach, nil
440
  }
441
 
442
  func (s *cvService) DeleteAchievement(ctx context.Context, id int64) error {
443
- return s.cvRepository.DeleteAchievement(ctx, id)
444
  }
 
1
  package services
2
 
3
  import (
4
+ "api.qobiltu.id/models"
5
+ "api.qobiltu.id/repositories"
6
+ "api.qobiltu.id/response"
7
+ "api.qobiltu.id/validation"
8
+ "context"
9
+ "errors"
10
+ "gorm.io/gorm"
11
  )
12
 
13
  type CVService interface {
14
+ SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error)
15
+ GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error)
16
+
17
+ SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error)
18
+ GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error)
19
+
20
+ CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
21
+ UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error)
22
+ ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
23
+ GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
24
+ DeleteFamilyMember(ctx context.Context, id int64) error
25
+
26
+ SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error)
27
+ GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error)
28
+
29
+ SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error)
30
+ GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error)
31
+
32
+ CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error)
33
+ UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error)
34
+ ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
35
+ GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
36
+ DeleteEducation(ctx context.Context, id int64) error
37
+
38
+ CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error)
39
+ UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error)
40
+ ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
41
+ GetJob(ctx context.Context, id int64) (*models.JobCV, error)
42
+ DeleteJob(ctx context.Context, id int64) error
43
+
44
+ CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error)
45
+ UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error)
46
+ ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
47
+ GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
48
+ DeleteAchievement(ctx context.Context, id int64) error
49
  }
50
 
51
  type cvService struct {
52
+ cvRepository repositories.CVRepository
53
  }
54
 
55
  func NewCVService(cvRepository repositories.CVRepository) CVService {
56
+ return &cvService{
57
+ cvRepository: cvRepository,
58
+ }
59
  }
60
 
61
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.AccountDetailsRequest) (*models.AccountDetails, error) {
62
+ if err := validation.Validate(req); err != nil {
63
+ return nil, response.HandleValidationError(err)
64
+ }
65
+
66
+ // Ambil data lama jika ada
67
+ accountDetails, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, req.AccountID)
68
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
69
+ return nil, response.HandleGormError(err, "Internal Server Error")
70
+ }
71
+
72
+ // Apply perubahan
73
+ if accountDetails == nil {
74
+ accountDetails = &models.AccountDetails{}
75
+ }
76
+
77
+ accountDetails.AccountID = uint(req.AccountID)
78
+ accountDetails.FullName = req.FullName
79
+ accountDetails.Gender = req.Gender
80
+ accountDetails.DateOfBirth = req.DateOfBirth
81
+ accountDetails.PlaceOfBirth = req.PlaceOfBirth
82
+ accountDetails.Domicile = req.Domicile
83
+ accountDetails.MaritalStatus = req.MaritalStatus
84
+ accountDetails.LastEducation = req.LastEducation
85
+ accountDetails.LastJob = req.LastJob
86
+
87
+ // Simpan data
88
+ res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
89
+ if err != nil {
90
+ return nil, response.HandleGormError(err, "Internal Server Error")
91
+ }
92
+
93
+ return res, nil
94
  }
95
 
96
  func (s *cvService) GetAccountDetails(ctx context.Context, id int64) (*models.AccountDetails, error) {
97
+ res, err := s.cvRepository.GetAccountDetailsByAccountID(ctx, id)
98
+ if err != nil {
99
+ return nil, response.HandleGormError(err, "Data diri tidak ditemukan")
100
+ }
101
+ return res, nil
102
  }
103
 
104
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCVRequest) (*models.PersonalityAndPreferenceCV, error) {
105
+ if err := validation.Validate(req); err != nil {
106
+ return nil, response.HandleValidationError(err)
107
+ }
108
+
109
+ // Ambil data lama jika ada
110
+ personalityAndPreference, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
111
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
112
+ return nil, response.HandleGormError(err, "Internal Server Error")
113
+ }
114
+
115
+ // Apply perubahan
116
+ if personalityAndPreference == nil {
117
+ personalityAndPreference = &models.PersonalityAndPreferenceCV{}
118
+ }
119
+
120
+ personalityAndPreference.AccountID = req.AccountID
121
+ personalityAndPreference.PositiveTraits = req.PositiveTraits
122
+ personalityAndPreference.NegativeTraits = req.NegativeTraits
123
+ personalityAndPreference.Hobbies = req.Hobbies
124
+ personalityAndPreference.LifeGoals = req.LifeGoals
125
+ personalityAndPreference.DailyActivities = req.DailyActivities
126
+ personalityAndPreference.LeisureActivities = req.LeisureActivities
127
+ personalityAndPreference.Likes = req.Likes
128
+ personalityAndPreference.Dislikes = req.Dislikes
129
+ personalityAndPreference.StressHandling = req.StressHandling
130
+ personalityAndPreference.AngerTriggers = req.AngerTriggers
131
+ personalityAndPreference.FavoriteFoodAndDrinks = req.FavoriteFoodAndDrinks
132
+ personalityAndPreference.CanCook = req.CanCook
133
+ personalityAndPreference.TypesOfDishesCooked = req.TypesOfDishesCooked
134
+ personalityAndPreference.MonthlyExpenses = req.MonthlyExpenses
135
+
136
+ res, err := s.cvRepository.SavePersonalityAndPreference(ctx, personalityAndPreference)
137
+ if err != nil {
138
+ return nil, response.HandleGormError(err, "Internal Server Error")
139
+ }
140
+
141
+ return res, nil
142
  }
143
 
144
  func (s *cvService) GetPersonalityAndPreference(ctx context.Context, id int64) (*models.PersonalityAndPreferenceCV, error) {
145
+ res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, id)
146
+ if err != nil {
147
+ return nil, response.HandleGormError(err, "Internal Server Error")
148
+ }
149
 
150
+ return res, nil
151
  }
152
 
153
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
154
+ if err := validation.Validate(req); err != nil {
155
+ return nil, response.HandleValidationError(err)
156
+ }
157
+
158
+ // Mapping request ke model
159
+ familyMember := &models.FamilyMemberCV{
160
+ AccountID: req.AccountID,
161
+ Role: req.Role,
162
+ Status: req.Status,
163
+ Religion: req.Religion,
164
+ Job: req.Job,
165
+ LastEducation: req.LastEducation,
166
+ Age: req.Age,
167
+ }
168
+
169
+ // Simpan ke repository
170
+ res, err := s.cvRepository.SaveFamilyMember(ctx, familyMember)
171
+ if err != nil {
172
+ return nil, response.HandleGormError(err, "Gagal menyimpan anggota keluarga")
173
+ }
174
+
175
+ return res, nil
176
  }
177
 
178
  func (s *cvService) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
179
+ list, err := s.cvRepository.ListFamilyMember(ctx, accountID)
180
+ if err != nil {
181
+ return nil, response.HandleGormError(err, "Gagal mengambil daftar anggota keluarga")
182
+ }
183
+ return list, nil
184
  }
185
 
186
  func (s *cvService) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
187
+ res, err := s.cvRepository.GetFamilyMember(ctx, id)
188
+ if err != nil {
189
+ return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
190
+ }
191
+ return res, nil
192
  }
193
 
194
  func (s *cvService) DeleteFamilyMember(ctx context.Context, id int64) error {
195
+ err := s.cvRepository.DeleteFamilyMember(ctx, id)
196
+ if err != nil {
197
+ return response.HandleGormError(err, "Gagal menghapus anggota keluarga")
198
+ }
199
+ return nil
200
  }
201
 
202
  func (s *cvService) UpdateFamilyMember(ctx context.Context, id int64, req *models.FamilyMemberRequest) (*models.FamilyMemberCV, error) {
203
+ if err := validation.Validate(req); err != nil {
204
+ return nil, response.HandleValidationError(err)
205
+ }
206
+
207
+ existing, err := s.cvRepository.GetFamilyMember(ctx, id)
208
+ if err != nil {
209
+ return nil, response.HandleGormError(err, "Data anggota keluarga tidak ditemukan")
210
+ }
211
+
212
+ existing.Role = req.Role
213
+ existing.Status = req.Status
214
+ existing.Religion = req.Religion
215
+ existing.Job = req.Job
216
+ existing.LastEducation = req.LastEducation
217
+ existing.Age = req.Age
218
+
219
+ updated, err := s.cvRepository.SaveFamilyMember(ctx, existing)
220
+ if err != nil {
221
+ return nil, response.HandleGormError(err, "Gagal memperbarui anggota keluarga")
222
+ }
223
+
224
+ return updated, nil
225
  }
226
 
227
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
228
+ if err := validation.Validate(req); err != nil {
229
+ return nil, response.HandleValidationError(err)
230
+ }
231
+
232
+ // Cek apakah data sudah ada berdasarkan account_id
233
+ existing, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
234
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
235
+ return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data fisik dan kesehatan")
236
+ }
237
+
238
+ // Jika belum ada, buat objek baru
239
+ if existing == nil {
240
+ existing = &models.PhysicalAndHealthCV{}
241
+ }
242
+
243
+ // Mapping field dari request
244
+ existing.AccountID = req.AccountID
245
+ existing.HeightInCm = req.HeightInCm
246
+ existing.WeightInKg = req.WeightInKg
247
+ existing.BodyShape = req.BodyShape
248
+ existing.SkinColor = req.SkinColor
249
+ existing.HairType = req.HairType
250
+ existing.MedicalHistory = req.MedicalHistory
251
+ existing.PhysicalDisorder = req.PhysicalDisorder
252
+ existing.PhysicalTraits = req.PhysicalTraits
253
+
254
+ // Simpan data
255
+ res, err := s.cvRepository.SavePhysicalAndHealth(ctx, existing)
256
+ if err != nil {
257
+ return nil, response.HandleGormError(err, "Gagal menyimpan data fisik dan kesehatan")
258
+ }
259
+
260
+ return res, nil
261
  }
262
 
263
  func (s *cvService) GetPhysicalAndHealth(ctx context.Context, id int64) (*models.PhysicalAndHealthCV, error) {
264
+ res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, id)
265
+ if err != nil {
266
+ return nil, response.HandleGormError(err, "Data fisik dan kesehatan tidak ditemukan")
267
+ }
268
+ return res, nil
269
  }
270
 
271
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
272
+ if err := validation.Validate(req); err != nil {
273
+ return nil, response.HandleValidationError(err)
274
+ }
275
+
276
+ // Cek apakah data sudah ada berdasarkan account_id
277
+ worshipAndReligiousUnderstanding, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
278
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
279
+ return nil, response.HandleGormError(err, "Terjadi kesalahan saat mengambil data agama dan pemahaman agama")
280
+ }
281
+
282
+ // Jika belum ada, buat objek baru
283
+ if worshipAndReligiousUnderstanding == nil {
284
+ worshipAndReligiousUnderstanding = &models.WorshipAndReligiousUnderstandingCV{}
285
+ }
286
+
287
+ // Mapping field dari request
288
+ worshipAndReligiousUnderstanding.AccountID = req.AccountID
289
+ worshipAndReligiousUnderstanding.ObligatoryPrayer = req.ObligatoryPrayer
290
+ worshipAndReligiousUnderstanding.CongregationalPrayer = req.CongregationalPrayer
291
+ worshipAndReligiousUnderstanding.TahajjudPrayer = req.TahajjudPrayer
292
+ worshipAndReligiousUnderstanding.DhuhaPrayer = req.DhuhaPrayer
293
+ worshipAndReligiousUnderstanding.QuranMemorization = req.QuranMemorization
294
+ worshipAndReligiousUnderstanding.QuranReadingAbility = req.QuranReadingAbility
295
+ worshipAndReligiousUnderstanding.DaudFasting = req.DaudFasting
296
+ worshipAndReligiousUnderstanding.AyyamulBidhFasting = req.AyyamulBidhFasting
297
+ worshipAndReligiousUnderstanding.HajjOrUmrah = req.HajjOrUmrah
298
+ worshipAndReligiousUnderstanding.ListeningToMusic = req.ListeningToMusic
299
+ worshipAndReligiousUnderstanding.OpinionOnIkhtilat = req.OpinionOnIkhtilat
300
+ worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram = req.OpinionOnTouchingNonMahram
301
+ worshipAndReligiousUnderstanding.OpinionOnVeil = req.OpinionOnVeil
302
+ worshipAndReligiousUnderstanding.WeeklyReligiousStudies = req.WeeklyReligiousStudies
303
+ worshipAndReligiousUnderstanding.FollowedUstadz = req.FollowedUstadz
304
+
305
+ // Simpan data
306
+ res, err := s.cvRepository.SaveWorshipAndReligiousUnderstanding(ctx, worshipAndReligiousUnderstanding)
307
+ if err != nil {
308
+ return nil, response.HandleGormError(err, "Internal Server Error")
309
+ }
310
+
311
+ return res, nil
312
  }
313
 
314
  func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, id int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
315
+ res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, id)
316
+ if err != nil {
317
+ return nil, response.HandleGormError(err, "Data agama dan pemahaman agama tidak ditemukan")
318
+ }
319
+ return res, nil
320
  }
321
 
322
  func (s *cvService) CreateEducation(ctx context.Context, req *models.EducationRequest) (*models.EducationCV, error) {
323
+ if err := validation.Validate(req); err != nil {
324
+ return nil, response.HandleValidationError(err)
325
+ }
326
+
327
+ edu := &models.EducationCV{
328
+ AccountID: req.AccountID,
329
+ LastEducation: req.LastEducation,
330
+ EducationInstitute: req.EducationInstitute,
331
+ EducationMajor: req.EducationMajor,
332
+ YearStart: req.YearStart,
333
+ YearGraduate: req.YearGraduate,
334
+ }
335
+
336
+ res, err := s.cvRepository.SaveEducation(ctx, edu)
337
+ if err != nil {
338
+ return nil, response.HandleGormError(err, "Gagal menambahkan data pendidikan")
339
+ }
340
+
341
+ return res, nil
342
  }
343
 
344
  func (s *cvService) UpdateEducation(ctx context.Context, id int64, req *models.EducationRequest) (*models.EducationCV, error) {
345
+ if err := validation.Validate(req); err != nil {
346
+ return nil, response.HandleValidationError(err)
347
+ }
348
+
349
+ edu, err := s.cvRepository.GetEducation(ctx, id)
350
+ if err != nil {
351
+ return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
352
+ }
353
+
354
+ edu.LastEducation = req.LastEducation
355
+ edu.EducationInstitute = req.EducationInstitute
356
+ edu.EducationMajor = req.EducationMajor
357
+ edu.YearStart = req.YearStart
358
+ edu.YearGraduate = req.YearGraduate
359
+
360
+ res, err := s.cvRepository.SaveEducation(ctx, edu)
361
+ if err != nil {
362
+ return nil, response.HandleGormError(err, "Gagal memperbarui data pendidikan")
363
+ }
364
+ return res, nil
365
  }
366
 
367
  func (s *cvService) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
368
+ return s.cvRepository.ListEducation(ctx, accountID)
369
  }
370
 
371
  func (s *cvService) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
372
+ edu, err := s.cvRepository.GetEducation(ctx, id)
373
+ if err != nil {
374
+ return nil, response.HandleGormError(err, "Data pendidikan tidak ditemukan")
375
+ }
376
+ return edu, nil
377
  }
378
 
379
  func (s *cvService) DeleteEducation(ctx context.Context, id int64) error {
380
+ return s.cvRepository.DeleteEducation(ctx, id)
381
  }
382
 
383
  func (s *cvService) CreateJob(ctx context.Context, req *models.JobRequest) (*models.JobCV, error) {
384
+ if err := validation.Validate(req); err != nil {
385
+ return nil, response.HandleValidationError(err)
386
+ }
387
+
388
+ job := &models.JobCV{
389
+ AccountID: req.AccountID,
390
+ InstitutionName: req.InstitutionName,
391
+ CurrentJob: req.CurrentJob,
392
+ YearStartedWorking: req.YearStartedWorking,
393
+ MonthlyIncome: req.MonthlyIncome,
394
+ IncomeSources: req.IncomeSources,
395
+ }
396
+ res, err := s.cvRepository.SaveJob(ctx, job)
397
+ if err != nil {
398
+ return nil, response.HandleGormError(err, "Gagal menambahkan data pekerjaan")
399
+ }
400
+ return res, nil
401
  }
402
 
403
  func (s *cvService) UpdateJob(ctx context.Context, id int64, req *models.JobRequest) (*models.JobCV, error) {
404
+ if err := validation.Validate(req); err != nil {
405
+ return nil, response.HandleValidationError(err)
406
+ }
407
+
408
+ job, err := s.cvRepository.GetJob(ctx, id)
409
+ if err != nil {
410
+ return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
411
+ }
412
+
413
+ job.InstitutionName = req.InstitutionName
414
+ job.CurrentJob = req.CurrentJob
415
+ job.YearStartedWorking = req.YearStartedWorking
416
+ job.MonthlyIncome = req.MonthlyIncome
417
+ job.IncomeSources = req.IncomeSources
418
+
419
+ res, err := s.cvRepository.SaveJob(ctx, job)
420
+ if err != nil {
421
+ return nil, response.HandleGormError(err, "Gagal memperbarui data pekerjaan")
422
+ }
423
+ return res, nil
424
  }
425
 
426
  func (s *cvService) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
427
+ return s.cvRepository.ListJob(ctx, accountID)
428
  }
429
 
430
  func (s *cvService) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
431
+ job, err := s.cvRepository.GetJob(ctx, id)
432
+ if err != nil {
433
+ return nil, response.HandleGormError(err, "Data pekerjaan tidak ditemukan")
434
+ }
435
+ return job, nil
436
  }
437
 
438
  func (s *cvService) DeleteJob(ctx context.Context, id int64) error {
439
+ return s.cvRepository.DeleteJob(ctx, id)
440
  }
441
 
442
  func (s *cvService) CreateAchievement(ctx context.Context, req *models.AchievementRequest) (*models.AchievementCV, error) {
443
+ ach := &models.AchievementCV{
444
+ AccountID: req.AccountID,
445
+ AchievementOrAward: req.AchievementOrAward,
446
+ }
447
+ res, err := s.cvRepository.SaveAchievement(ctx, ach)
448
+ if err != nil {
449
+ return nil, response.HandleGormError(err, "Gagal menambahkan data prestasi")
450
+ }
451
+
452
+ return res, nil
453
  }
454
 
455
  func (s *cvService) UpdateAchievement(ctx context.Context, id int64, req *models.AchievementRequest) (*models.AchievementCV, error) {
456
+ ach, err := s.cvRepository.GetAchievement(ctx, id)
457
+ if err != nil {
458
+ return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
459
+ }
460
 
461
+ ach.AchievementOrAward = req.AchievementOrAward
462
 
463
+ res, err := s.cvRepository.SaveAchievement(ctx, ach)
464
+ if err != nil {
465
+ return nil, response.HandleGormError(err, "Gagal memperbarui data prestasi")
466
+ }
467
 
468
+ return res, nil
469
  }
470
 
471
  func (s *cvService) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
472
+ return s.cvRepository.ListAchievement(ctx, accountID)
473
  }
474
 
475
  func (s *cvService) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
476
+ ach, err := s.cvRepository.GetAchievement(ctx, id)
477
+ if err != nil {
478
+ return nil, response.HandleGormError(err, "Data prestasi tidak ditemukan")
479
+ }
480
+ return ach, nil
481
  }
482
 
483
  func (s *cvService) DeleteAchievement(ctx context.Context, id int64) error {
484
+ return s.cvRepository.DeleteAchievement(ctx, id)
485
  }
space/space/space/space/space/space/go.sum CHANGED
@@ -97,6 +97,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
97
  github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
98
  github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
99
  github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 
 
100
  github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
101
  github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
102
  github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 
97
  github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
98
  github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
99
  github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
100
+ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
101
+ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
102
  github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
103
  github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
104
  github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -3,6 +3,7 @@ package models
3
  import (
4
  "time"
5
 
 
6
  uuid "github.com/satori/go.uuid"
7
  )
8
 
@@ -241,33 +242,33 @@ type (
241
  }
242
 
243
  WorshipAndReligiousUnderstandingCV struct {
244
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
245
- AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id"`
246
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
247
- ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
248
- CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
249
- TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
250
- DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
251
- QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
252
- QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
253
- DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
254
- AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
255
- HajjOrUmrah *string `gorm:"column:hajj_or_umrah" json:"hajj_or_umrah"` // ibadah_haji_umroh
256
- ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
257
- OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
258
- OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
259
- OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
260
- WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
261
- FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
262
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
263
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
264
  }
265
 
266
  EducationCV struct {
267
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // id
268
  AccountID int64 `gorm:"column:account_id;not null" json:"account_id"` // id akun
269
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"` // relasi ke akun
270
- LastEducation *string `gorm:"column:last_education" json:"last_education"` // pendidikan terakhir
271
  EducationInstitute *string `gorm:"column:education_institute" json:"education_institute"` // institusi pendidikan
272
  EducationMajor *string `gorm:"column:education_major" json:"education_major"` // jurusan pendidikan
273
  YearStart *int `gorm:"column:year_start" json:"year_start"` // tahun masuk
@@ -277,16 +278,16 @@ type (
277
  }
278
 
279
  JobCV struct {
280
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // id
281
- AccountID int64 `gorm:"column:account_id;not null" json:"account_id"` // id akun
282
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"` // relasi ke akun
283
- InstitutionName *string `gorm:"column:institution_name" json:"institution_name"` // nama instansi
284
- CurrentJob *string `gorm:"column:current_job" json:"current_job"` // pekerjaan saat ini
285
- YearStartedWorking *int `gorm:"column:year_started_working" json:"year_started_working"` // tahun mulai bekerja
286
- MonthlyIncome *string `gorm:"column:monthly_income" json:"monthly_income"` // penghasilan per bulan
287
- IncomeSources *string `gorm:"column:income_sources" json:"income_sources"` // sumber penghasilan
288
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // tanggal dibuat
289
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // tanggal diperbarui
290
  }
291
 
292
  AchievementCV struct {
 
3
  import (
4
  "time"
5
 
6
+ "github.com/lib/pq"
7
  uuid "github.com/satori/go.uuid"
8
  )
9
 
 
242
  }
243
 
244
  WorshipAndReligiousUnderstandingCV struct {
245
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
246
+ AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id"`
247
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
248
+ ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
249
+ CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
250
+ TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
251
+ DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
252
+ QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
253
+ QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
254
+ DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
255
+ AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
256
+ HajjOrUmrah pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
257
+ ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
258
+ OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
259
+ OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
260
+ OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
261
+ WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
262
+ FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
263
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
264
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
265
  }
266
 
267
  EducationCV struct {
268
  ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // id
269
  AccountID int64 `gorm:"column:account_id;not null" json:"account_id"` // id akun
270
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"` // relasi ke akun
271
+ LastEducation *string `gorm:"column:last_education" json:"last_education" validate:""` // pendidikan terakhir
272
  EducationInstitute *string `gorm:"column:education_institute" json:"education_institute"` // institusi pendidikan
273
  EducationMajor *string `gorm:"column:education_major" json:"education_major"` // jurusan pendidikan
274
  YearStart *int `gorm:"column:year_start" json:"year_start"` // tahun masuk
 
278
  }
279
 
280
  JobCV struct {
281
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // id
282
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"` // id akun
283
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"` // relasi ke akun
284
+ InstitutionName *string `gorm:"column:institution_name" json:"institution_name"` // nama instansi
285
+ CurrentJob *string `gorm:"column:current_job" json:"current_job"` // pekerjaan saat ini
286
+ YearStartedWorking *int `gorm:"column:year_started_working" json:"year_started_working"` // tahun mulai bekerja
287
+ MonthlyIncome *string `gorm:"column:monthly_income" json:"monthly_income"` // penghasilan per bulan
288
+ IncomeSources pq.StringArray `gorm:"column:income_sources;type:varchar(255)[]" json:"income_sources"` // sumber penghasilan
289
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` // tanggal dibuat
290
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"` // tanggal diperbarui
291
  }
292
 
293
  AchievementCV struct {
space/space/space/space/space/space/models/request_model.go CHANGED
@@ -1,6 +1,9 @@
1
  package models
2
 
3
- import "time"
 
 
 
4
 
5
  type LoginRequest struct {
6
  Email string `json:"email" binding:"required"`
@@ -55,91 +58,91 @@ type AnswerQuizRequest struct {
55
  type (
56
  PersonalityAndPreferenceCVRequest struct {
57
  AccountID int64 `json:"-"`
58
- PositiveTraits *string `json:"positive_traits"` // sifat positif
59
- NegativeTraits *string `json:"negative_traits"` // sifat negatif
60
- Hobbies *string `json:"hobbies"` // hobi
61
- LifeGoals *string `json:"life_goals"` // target hidup
62
- DailyActivities *string `json:"daily_activities"` // kegiatan sehari-hari
63
- LeisureActivities *string `json:"leisure_activities"` // kegiatan waktu luang
64
- Likes *string `json:"likes"` // hal yang disukai
65
- Dislikes *string `json:"dislikes"` // hal yang tidak disukai
66
- StressHandling *string `json:"stress_handling"` // cara mengatasi stres
67
- AngerTriggers *string `json:"anger_triggers"` // pemicu amarah
68
- FavoriteFoodAndDrinks *string `json:"favorite_food_and_drinks"` // makanan dan minuman favorit
69
- CanCook *bool `json:"can_cook"` // bisa memasak
70
- TypesOfDishesCooked *string `json:"types_of_dishes_cooked"` // jenis masakan yang bisa dimasak
71
- MonthlyExpenses *string `json:"monthly_expenses"` // pengeluaran per bulan
72
  }
73
 
74
  FamilyMemberRequest struct {
75
  AccountID int64 `json:"-"`
76
- Role *string `json:"role"` // Peran dalam keluarga
77
- Status *string `json:"status"` // Status (Hidup, Wafat)
78
- Religion *string `json:"religion"` // Agama
79
- Job *string `json:"job"` // Pekerjaan
80
- LastEducation *string `json:"last_education"` // Pendidikan terakhir
81
- Age *int `json:"age"` // Usia
82
  }
83
 
84
  PhysicalAndHealthRequest struct {
85
  AccountID int64 `json:"-"`
86
- HeightInCm *int `json:"height_cm"` // Tinggi badan dalam satuan sentimeter
87
- WeightInKg *int `json:"weight_kg"` // Berat badan dalam satuan kilogram
88
- BodyShape *string `json:"body_shape"` // Bentuk tubuh
89
- SkinColor *string `json:"skin_color"` // Warna kulit
90
- HairType *string `json:"hair_type"` // Tipe rambut
91
- MedicalHistory *string `json:"medical_history"` // Riwayat penyakit
92
- PhysicalDisorder *string `json:"physical_disorder"` // Cacat fisik
93
- PhysicalTraits *string `json:"physical_traits"` // Ciri khas fisik
94
  }
95
 
96
  AccountDetailsRequest struct {
97
  AccountID int64 `json:"-"`
98
  FullName *string `json:"full_name"`
99
- Gender *string `json:"gender"`
100
  DateOfBirth *time.Time `json:"date_of_birth"`
101
  PlaceOfBirth *string `json:"place_of_birth"`
102
  Domicile *string `json:"domicile"`
103
- MaritalStatus *string `json:"marital_status"`
104
- LastEducation *string `json:"last_education"`
105
  LastJob *string `json:"last_job"`
106
  }
107
 
108
  WorshipAndReligiousUnderstandingRequest struct {
109
- AccountID int64 `json:"-"`
110
- ObligatoryPrayer *string `json:"obligatory_prayer"` // sholat_wajib_5_waktu
111
- CongregationalPrayer *string `json:"congregational_prayer"` // sholat_berjamaah_di_masjid
112
- TahajjudPrayer *string `json:"tahajjud_prayer"` // sholat_tahajud
113
- DhuhaPrayer *string `json:"dhuha_prayer"` // sholat_dhuha
114
- QuranMemorization *string `json:"quran_memorization"` // hafalan_alquran
115
- QuranReadingAbility *string `json:"quran_reading_ability"` // kemampuan_baca_alquran
116
- DaudFasting *string `json:"daud_fasting"` // puasa_daud
117
- AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
118
- HajjOrUmrah *string `json:"hajj_or_umrah"` // ibadah_haji_umroh
119
- ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
120
- OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
121
- OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
122
- OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
123
- WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
124
- FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
125
  }
126
 
127
  EducationRequest struct {
128
  AccountID int64 `json:"account_id"`
129
- LastEducation *string `json:"last_education"` // pendidikan terakhir
130
- EducationInstitute *string `json:"education_institute"` // institusi pendidikan
131
- EducationMajor *string `json:"education_major"` // jurusan pendidikan
132
- YearStart *int `json:"year_start"` // tahun masuk
133
- YearGraduate *int `json:"year_graduate"` // tahun lulus
134
  }
135
 
136
  JobRequest struct {
137
- AccountID int64 `json:"account_id"`
138
- InstitutionName *string `json:"institution_name"` // nama instansi
139
- CurrentJob *string `json:"current_job"` // pekerjaan saat ini
140
- YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
141
- MonthlyIncome *string `json:"monthly_income"` // penghasilan per bulan
142
- IncomeSources *string `json:"income_sources"` // sumber penghasilan
143
  }
144
 
145
  AchievementRequest struct {
 
1
  package models
2
 
3
+ import (
4
+ "github.com/lib/pq"
5
+ "time"
6
+ )
7
 
8
  type LoginRequest struct {
9
  Email string `json:"email" binding:"required"`
 
58
  type (
59
  PersonalityAndPreferenceCVRequest struct {
60
  AccountID int64 `json:"-"`
61
+ PositiveTraits *string `json:"positive_traits"` // sifat positif
62
+ NegativeTraits *string `json:"negative_traits"` // sifat negatif
63
+ Hobbies *string `json:"hobbies"` // hobi
64
+ LifeGoals *string `json:"life_goals"` // target hidup
65
+ DailyActivities *string `json:"daily_activities"` // kegiatan sehari-hari
66
+ LeisureActivities *string `json:"leisure_activities"` // kegiatan waktu luang
67
+ Likes *string `json:"likes"` // hal yang disukai
68
+ Dislikes *string `json:"dislikes"` // hal yang tidak disukai
69
+ StressHandling *string `json:"stress_handling"` // cara mengatasi stres
70
+ AngerTriggers *string `json:"anger_triggers"` // pemicu amarah
71
+ FavoriteFoodAndDrinks *string `json:"favorite_food_and_drinks"` // makanan dan minuman favorit
72
+ CanCook *bool `json:"can_cook"` // bisa memasak
73
+ TypesOfDishesCooked *string `json:"types_of_dishes_cooked"` // jenis masakan yang bisa dimasak
74
+ MonthlyExpenses *string `json:"monthly_expenses" validate:"monthly_expenses"` // pengeluaran per bulan
75
  }
76
 
77
  FamilyMemberRequest struct {
78
  AccountID int64 `json:"-"`
79
+ Role *string `json:"role" validate:"family_role"` // Peran dalam keluarga
80
+ Status *string `json:"status" validate:"life_status"` // Status (Hidup, Wafat)
81
+ Religion *string `json:"religion" validate:"religion"` // Agama
82
+ Job *string `json:"job"` // Pekerjaan
83
+ LastEducation *string `json:"last_education" validate:"last_education"` // Pendidikan terakhir
84
+ Age *int `json:"age"` // Usia
85
  }
86
 
87
  PhysicalAndHealthRequest struct {
88
  AccountID int64 `json:"-"`
89
+ HeightInCm *int `json:"height_cm"` // Tinggi badan dalam satuan sentimeter
90
+ WeightInKg *int `json:"weight_kg"` // Berat badan dalam satuan kilogram
91
+ BodyShape *string `json:"body_shape" validate:"body_shape"` // Bentuk tubuh
92
+ SkinColor *string `json:"skin_color" validate:"skin_color"` // Warna kulit
93
+ HairType *string `json:"hair_type" validate:"hair_type"` // Tipe rambut
94
+ MedicalHistory *string `json:"medical_history"` // Riwayat penyakit
95
+ PhysicalDisorder *string `json:"physical_disorder"` // Cacat fisik
96
+ PhysicalTraits *string `json:"physical_traits"` // Ciri khas fisik
97
  }
98
 
99
  AccountDetailsRequest struct {
100
  AccountID int64 `json:"-"`
101
  FullName *string `json:"full_name"`
102
+ Gender *string `json:"gender" validate:"gender"`
103
  DateOfBirth *time.Time `json:"date_of_birth"`
104
  PlaceOfBirth *string `json:"place_of_birth"`
105
  Domicile *string `json:"domicile"`
106
+ MaritalStatus *string `json:"marital_status" validate:"marital_status"`
107
+ LastEducation *string `json:"last_education" validate:"last_education"`
108
  LastJob *string `json:"last_job"`
109
  }
110
 
111
  WorshipAndReligiousUnderstandingRequest struct {
112
+ AccountID int64 `json:"-"`
113
+ ObligatoryPrayer *string `json:"obligatory_prayer"` // sholat_wajib_5_waktu
114
+ CongregationalPrayer *string `json:"congregational_prayer"` // sholat_berjamaah_di_masjid
115
+ TahajjudPrayer *string `json:"tahajjud_prayer"` // sholat_tahajud
116
+ DhuhaPrayer *string `json:"dhuha_prayer"` // sholat_dhuha
117
+ QuranMemorization *string `json:"quran_memorization"` // hafalan_alquran
118
+ QuranReadingAbility *string `json:"quran_reading_ability" validate:"quran_reading_ability"` // kemampuan_baca_alquran
119
+ DaudFasting *string `json:"daud_fasting"` // puasa_daud
120
+ AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
121
+ HajjOrUmrah pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
122
+ ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
123
+ OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
124
+ OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
125
+ OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
126
+ WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
127
+ FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
128
  }
129
 
130
  EducationRequest struct {
131
  AccountID int64 `json:"account_id"`
132
+ LastEducation *string `json:"last_education" validate:"last_education"` // pendidikan terakhir
133
+ EducationInstitute *string `json:"education_institute"` // institusi pendidikan
134
+ EducationMajor *string `json:"education_major"` // jurusan pendidikan
135
+ YearStart *int `json:"year_start"` // tahun masuk
136
+ YearGraduate *int `json:"year_graduate"` // tahun lulus
137
  }
138
 
139
  JobRequest struct {
140
+ AccountID int64 `json:"account_id"`
141
+ InstitutionName *string `json:"institution_name"` // nama instansi
142
+ CurrentJob *string `json:"current_job"` // pekerjaan saat ini
143
+ YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
144
+ MonthlyIncome *string `json:"monthly_income" validate:"monthly_income"` // penghasilan per bulan
145
+ IncomeSources pq.StringArray `json:"income_sources"` // sumber penghasilan
146
  }
147
 
148
  AchievementRequest struct {
space/space/space/space/space/space/space/controller/quiz/list_quiz_controller.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controller
2
+
3
+ import (
4
+ "strconv"
5
+
6
+ "api.qobiltu.id/controller"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/services"
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ func List(c *gin.Context) {
13
+ quizList := services.QuizListService{}
14
+ quizListController := controller.Controller[any, models.Academy, []models.Quiz]{
15
+ Service: &quizList.Service,
16
+ }
17
+ quizListController.HeaderParse(c, func() {
18
+ academy_id, _ := strconv.Atoi(c.Param("academy_id"))
19
+ quizList.Constructor.ID = uint(academy_id)
20
+ quizList.Retrieve()
21
+ quizListController.Response(c)
22
+ })
23
+ }
space/space/space/space/space/space/space/go.mod CHANGED
@@ -4,11 +4,15 @@ go 1.24.0
4
 
5
  require (
6
  github.com/gin-gonic/gin v1.10.0
 
 
 
7
  github.com/golang-jwt/jwt/v5 v5.2.1
8
  github.com/gosimple/slug v1.15.0
9
  github.com/hibiken/asynq v0.25.1
10
  github.com/joho/godotenv v1.5.1
11
  github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
 
12
  github.com/redis/go-redis/v9 v9.7.0
13
  github.com/satori/go.uuid v1.2.0
14
  golang.org/x/crypto v0.36.0
@@ -31,9 +35,6 @@ require (
31
  github.com/gin-contrib/sse v1.0.0 // indirect
32
  github.com/go-logr/logr v1.4.2 // indirect
33
  github.com/go-logr/stdr v1.2.2 // indirect
34
- github.com/go-playground/locales v0.14.1 // indirect
35
- github.com/go-playground/universal-translator v0.18.1 // indirect
36
- github.com/go-playground/validator/v10 v10.25.0 // indirect
37
  github.com/goccy/go-json v0.10.5 // indirect
38
  github.com/google/s2a-go v0.1.9 // indirect
39
  github.com/google/uuid v1.6.0 // indirect
 
4
 
5
  require (
6
  github.com/gin-gonic/gin v1.10.0
7
+ github.com/go-playground/locales v0.14.1
8
+ github.com/go-playground/universal-translator v0.18.1
9
+ github.com/go-playground/validator/v10 v10.25.0
10
  github.com/golang-jwt/jwt/v5 v5.2.1
11
  github.com/gosimple/slug v1.15.0
12
  github.com/hibiken/asynq v0.25.1
13
  github.com/joho/godotenv v1.5.1
14
  github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
15
+ github.com/lib/pq v1.10.9
16
  github.com/redis/go-redis/v9 v9.7.0
17
  github.com/satori/go.uuid v1.2.0
18
  golang.org/x/crypto v0.36.0
 
35
  github.com/gin-contrib/sse v1.0.0 // indirect
36
  github.com/go-logr/logr v1.4.2 // indirect
37
  github.com/go-logr/stdr v1.2.2 // indirect
 
 
 
38
  github.com/goccy/go-json v0.10.5 // indirect
39
  github.com/google/s2a-go v0.1.9 // indirect
40
  github.com/google/uuid v1.6.0 // indirect
space/space/space/space/space/space/space/router/quiz_route.go CHANGED
@@ -9,6 +9,7 @@ import (
9
  func QuizRoute(router *gin.Engine) {
10
  routerGroup := router.Group("/api/v1/quiz")
11
  {
 
12
  routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
13
  routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
14
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
 
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.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)