lifedebugger commited on
Commit
3ccc959
·
1 Parent(s): abe4f0d

Deploy files from GitHub repository

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. main.go +1 -1
  2. pkg/mail/sender.go +8 -0
  3. pkg/mail/smtp.go +71 -0
  4. pkg/worker/processor.go +3 -2
  5. router/email_route.go +2 -3
  6. router/router.go +1 -1
  7. space/main.go +1 -1
  8. space/pkg/worker/processor.go +3 -2
  9. space/router/email_route.go +2 -3
  10. space/router/router.go +1 -1
  11. space/space/controller/auth/auth_login_controller.go +1 -1
  12. space/space/controller/auth/auth_register_controller.go +1 -1
  13. space/space/controller/email/email_controller.go +64 -0
  14. space/space/main.go +7 -1
  15. space/space/models/database_orm_model.go +7 -5
  16. space/space/models/request_model.go +11 -0
  17. space/space/repositories/email_repository.go +65 -0
  18. space/space/router/email_route.go +4 -6
  19. space/space/router/router.go +1 -2
  20. space/space/router/server.go +4 -0
  21. space/space/services/email_service.go +123 -0
  22. space/space/space/controller/quiz/review_quiz_controller.go +26 -0
  23. space/space/space/router/quiz_route.go +1 -0
  24. space/space/space/services/academy_quiz_question_service.go +2 -0
  25. space/space/space/services/academy_quiz_review_service.go +56 -0
  26. space/space/space/services/academy_quiz_service.go +2 -5
  27. space/space/space/space/main.go +0 -6
  28. space/space/space/space/models/database_orm_model.go +12 -7
  29. space/space/space/space/models/request_model.go +8 -6
  30. space/space/space/space/router/router.go +0 -1
  31. space/space/space/space/router/server.go +0 -4
  32. space/space/space/space/services/partner_criteria_service.go +4 -2
  33. space/space/space/space/space/pkg/validation/validation.go +16 -155
  34. space/space/space/space/space/response/validation.go +19 -2
  35. space/space/space/space/space/services/marriage_readiness_profile_service.go +4 -3
  36. space/space/space/space/space/space/controller/partner_criteria/partner_criteria_controller.go +66 -0
  37. space/space/space/space/space/space/main.go +9 -3
  38. space/space/space/space/space/space/models/database_orm_model.go +54 -20
  39. space/space/space/space/space/space/models/request_model.go +112 -77
  40. space/space/space/space/space/space/pkg/validation/custom_rules.go +236 -116
  41. space/space/space/space/space/space/repositories/partner_criteria_repository.go +38 -0
  42. space/space/space/space/space/space/router/partner_criteria_route.go +11 -0
  43. space/space/space/space/space/space/router/router.go +1 -0
  44. space/space/space/space/space/space/router/server.go +4 -0
  45. space/space/space/space/space/space/services/partner_criteria_service.go +78 -0
  46. space/space/space/space/space/space/space/config/database_connection_config.go +13 -2
  47. space/space/space/space/space/space/space/models/sequence.go +25 -0
  48. space/space/space/space/space/space/space/repositories/account_repository.go +18 -0
  49. space/space/space/space/space/space/space/repositories/cv_repository.go +162 -146
  50. space/space/space/space/space/space/space/services/cv_service.go +49 -20
main.go CHANGED
@@ -10,7 +10,7 @@ import (
10
  email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
- "api.qobiltu.id/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
16
  "api.qobiltu.id/pkg/worker"
 
10
  email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
+ "api.qobiltu.id/pkg/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
16
  "api.qobiltu.id/pkg/worker"
pkg/mail/sender.go ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package mail
2
+
3
+ type Sender interface {
4
+ Send(recipient, subject string, htmlContent string, data any) error
5
+ }
6
+
7
+ // EmailSender is a global variable to hold the email sender
8
+ var EmailSender Sender
pkg/mail/smtp.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package mail
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "github.com/jordan-wright/email"
7
+ "net/mail"
8
+ "net/smtp"
9
+ )
10
+
11
+ var (
12
+ ErrEmailEmpty = errors.New("mail is empty")
13
+ ErrEmailInvalid = errors.New("invalid email")
14
+ )
15
+
16
+ // Config menyimpan pengaturan untuk koneksi SMTP.
17
+ type Config struct {
18
+ Host string
19
+ Port string
20
+ Username string
21
+ Password string
22
+ From string
23
+ }
24
+
25
+ // SMTP adalah implementasi Sender untuk mengirim mail melalui SMTP
26
+ type SMTP struct {
27
+ name string
28
+ fromEmailAddress string
29
+ smtpServerAddress string
30
+ smtpAuthAddress smtp.Auth
31
+ }
32
+
33
+ // New membuat instance baru SMTP dengan konfigurasi.
34
+ func New(cfg *Config) (Sender, error) {
35
+ if err := validateEmail(cfg.From); err != nil {
36
+ return nil, fmt.Errorf("invalid from address: %w", err)
37
+ }
38
+
39
+ return &SMTP{
40
+ name: cfg.From,
41
+ fromEmailAddress: cfg.From,
42
+ smtpServerAddress: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
43
+ smtpAuthAddress: smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host),
44
+ }, nil
45
+ }
46
+
47
+ // Send mengirim mail ke penerima dengan subjek, konten HTML, dan data lainnya.
48
+ func (s *SMTP) Send(recipient, subject string, htmlContent string, data any) error {
49
+ e := email.NewEmail()
50
+ e.From = s.fromEmailAddress
51
+ e.To = []string{recipient}
52
+ e.Subject = subject
53
+ e.HTML = []byte(htmlContent)
54
+ return s.sendEmail(e)
55
+ }
56
+
57
+ func (s *SMTP) sendEmail(e *email.Email) error {
58
+ return e.Send(s.smtpServerAddress, s.smtpAuthAddress)
59
+ }
60
+
61
+ func validateEmail(email string) error {
62
+ if email == "" {
63
+ return ErrEmailEmpty
64
+ }
65
+
66
+ if _, err := mail.ParseAddress(email); err != nil {
67
+ return ErrEmailInvalid
68
+ }
69
+
70
+ return nil
71
+ }
pkg/worker/processor.go CHANGED
@@ -1,11 +1,12 @@
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 (
 
1
  package worker
2
 
3
  import (
 
4
  "context"
5
+ "log/slog"
6
+
7
+ "api.qobiltu.id/pkg/mail"
8
  "github.com/hibiken/asynq"
9
  "github.com/redis/go-redis/v9"
 
10
  )
11
 
12
  const (
router/email_route.go CHANGED
@@ -2,11 +2,10 @@ package router
2
 
3
  import (
4
  "api.qobiltu.id/middleware"
5
- "github.com/gin-gonic/gin"
6
  )
7
 
8
- func (s *Server) EmailRoute(router *gin.Engine) {
9
- routerGroup := router.Group("/api/v1/email").Use(middleware.AuthUser)
10
  {
11
  routerGroup.POST("/create-verification", s.emailController.CreateEmailVerification)
12
  routerGroup.POST("/verify", s.emailController.Verify)
 
2
 
3
  import (
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) EmailRoute() {
8
+ routerGroup := s.router.Group("/api/v1/email").Use(middleware.AuthUser)
9
  {
10
  routerGroup.POST("/create-verification", s.emailController.CreateEmailVerification)
11
  routerGroup.POST("/verify", s.emailController.Verify)
router/router.go CHANGED
@@ -14,7 +14,7 @@ func (s *Server) setupRoutes() {
14
  AcademyRoute(s.router)
15
  QuizRoute(s.router)
16
 
17
- s.EmailRoute(s.router)
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
 
14
  AcademyRoute(s.router)
15
  QuizRoute(s.router)
16
 
17
+ s.EmailRoute()
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
space/main.go CHANGED
@@ -10,7 +10,7 @@ import (
10
  email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
- "api.qobiltu.id/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
16
  "api.qobiltu.id/pkg/worker"
 
10
  email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
+ "api.qobiltu.id/pkg/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
16
  "api.qobiltu.id/pkg/worker"
space/pkg/worker/processor.go CHANGED
@@ -1,11 +1,12 @@
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 (
 
1
  package worker
2
 
3
  import (
 
4
  "context"
5
+ "log/slog"
6
+
7
+ "api.qobiltu.id/pkg/mail"
8
  "github.com/hibiken/asynq"
9
  "github.com/redis/go-redis/v9"
 
10
  )
11
 
12
  const (
space/router/email_route.go CHANGED
@@ -2,11 +2,10 @@ package router
2
 
3
  import (
4
  "api.qobiltu.id/middleware"
5
- "github.com/gin-gonic/gin"
6
  )
7
 
8
- func (s *Server) EmailRoute(router *gin.Engine) {
9
- routerGroup := router.Group("/api/v1/email").Use(middleware.AuthUser)
10
  {
11
  routerGroup.POST("/create-verification", s.emailController.CreateEmailVerification)
12
  routerGroup.POST("/verify", s.emailController.Verify)
 
2
 
3
  import (
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) EmailRoute() {
8
+ routerGroup := s.router.Group("/api/v1/email").Use(middleware.AuthUser)
9
  {
10
  routerGroup.POST("/create-verification", s.emailController.CreateEmailVerification)
11
  routerGroup.POST("/verify", s.emailController.Verify)
space/router/router.go CHANGED
@@ -14,7 +14,7 @@ func (s *Server) setupRoutes() {
14
  AcademyRoute(s.router)
15
  QuizRoute(s.router)
16
 
17
- s.EmailRoute(s.router)
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
 
14
  AcademyRoute(s.router)
15
  QuizRoute(s.router)
16
 
17
+ s.EmailRoute()
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
space/space/controller/auth/auth_login_controller.go CHANGED
@@ -1,4 +1,4 @@
1
- package user
2
 
3
  import (
4
  "api.qobiltu.id/controller"
 
1
+ package auth
2
 
3
  import (
4
  "api.qobiltu.id/controller"
space/space/controller/auth/auth_register_controller.go CHANGED
@@ -1,4 +1,4 @@
1
- package user
2
 
3
  import (
4
  "api.qobiltu.id/controller"
 
1
+ package auth
2
 
3
  import (
4
  "api.qobiltu.id/controller"
space/space/controller/email/email_controller.go ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package email_controller
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "api.qobiltu.id/middleware"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/response"
9
+ "api.qobiltu.id/services"
10
+ "github.com/gin-gonic/gin"
11
+ )
12
+
13
+ type EmailController interface {
14
+ CreateEmailVerification(ctx *gin.Context)
15
+ Verify(ctx *gin.Context)
16
+ }
17
+
18
+ type emailController struct {
19
+ emailService services.EmailService
20
+ }
21
+
22
+ func NewEmailController(emailService services.EmailService) EmailController {
23
+ return &emailController{
24
+ emailService: emailService,
25
+ }
26
+ }
27
+
28
+ func (c *emailController) CreateEmailVerification(ctx *gin.Context) {
29
+ accountData := middleware.GetAccountData(ctx)
30
+ req := models.CreateEmailVerificationRequest{
31
+ AccountID: int64(accountData.UserID),
32
+ }
33
+
34
+ res, err := c.emailService.CreateEmailVerification(ctx, &req)
35
+ if err != nil {
36
+ response.HandleError(ctx, err)
37
+ return
38
+ }
39
+
40
+ response.HandleSuccess(ctx, http.StatusOK, "Email verification created", res, nil)
41
+ }
42
+
43
+ func (c *emailController) Verify(ctx *gin.Context) {
44
+ var req models.ValidateEmailVerificationRequest
45
+ if err := ctx.ShouldBindJSON(&req); err != nil {
46
+ response.HandleError(ctx, models.Exception{
47
+ Message: "Invalid body request",
48
+ BadRequest: true,
49
+ Err: err,
50
+ })
51
+ return
52
+ }
53
+
54
+ accountData := middleware.GetAccountData(ctx)
55
+ req.AccountID = int64(accountData.UserID)
56
+
57
+ res, err := c.emailService.ValidateEmailVerification(ctx, &req)
58
+ if err != nil {
59
+ response.HandleError(ctx, err)
60
+ return
61
+ }
62
+
63
+ response.HandleSuccess(ctx, http.StatusOK, "Email verified", res, nil)
64
+ }
space/space/main.go CHANGED
@@ -7,9 +7,10 @@ import (
7
 
8
  "api.qobiltu.id/config"
9
  cv_controller "api.qobiltu.id/controller/cv"
 
10
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
11
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
12
- "api.qobiltu.id/mail"
13
  "api.qobiltu.id/pkg/storage"
14
  "api.qobiltu.id/pkg/validation"
15
  "api.qobiltu.id/pkg/worker"
@@ -54,6 +55,10 @@ func main() {
54
  worker.AsyncTaskDistributor = taskDistributor
55
 
56
  // setup repo, service, and controller
 
 
 
 
57
  cvRepository := repositories.NewCVRepository(config.DB)
58
  cvService := services.NewCVService(cvRepository, localStorage, validator)
59
  cvController := cv_controller.NewCVController(cvService)
@@ -73,6 +78,7 @@ func main() {
73
 
74
  // create server
75
  s, err := router.NewServer(
 
76
  cvController,
77
  marriageReadinessProfileController,
78
  partnerCriteriaController,
 
7
 
8
  "api.qobiltu.id/config"
9
  cv_controller "api.qobiltu.id/controller/cv"
10
+ email_controller "api.qobiltu.id/controller/email"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
+ "api.qobiltu.id/pkg/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
16
  "api.qobiltu.id/pkg/worker"
 
55
  worker.AsyncTaskDistributor = taskDistributor
56
 
57
  // setup repo, service, and controller
58
+ emailRepository := repositories.NewEmailRepository(config.DB)
59
+ emailService := services.NewEmailService(emailRepository, taskDistributor)
60
+ emailController := email_controller.NewEmailController(emailService)
61
+
62
  cvRepository := repositories.NewCVRepository(config.DB)
63
  cvService := services.NewCVService(cvRepository, localStorage, validator)
64
  cvController := cv_controller.NewCVController(cvService)
 
78
 
79
  // create server
80
  s, err := router.NewServer(
81
+ emailController,
82
  cvController,
83
  marriageReadinessProfileController,
84
  partnerCriteriaController,
space/space/models/database_orm_model.go CHANGED
@@ -38,13 +38,15 @@ type AccountDetails struct {
38
  }
39
 
40
  type EmailVerification struct {
41
- ID uint `gorm:"primaryKey" json:"id"`
 
 
42
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
43
- Token uint `json:"token"`
44
- AccountID uint `json:"account_id"`
45
- IsExpired bool `json:"is_expired"`
46
- CreatedAt time.Time `json:"created_at"`
47
  ExpiredAt time.Time `json:"expired_at"`
 
 
 
48
  }
49
 
50
  type ExternalAuth struct {
 
38
  }
39
 
40
  type EmailVerification struct {
41
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
42
+ AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
43
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
44
  UUID uuid.UUID `gorm:"type:uuid" json:"uuid" `
45
+ Token int64 `json:"token"`
 
 
 
46
  ExpiredAt time.Time `json:"expired_at"`
47
+ IsExpired bool `json:"is_expired"`
48
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
49
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
50
  }
51
 
52
  type ExternalAuth struct {
space/space/models/request_model.go CHANGED
@@ -57,6 +57,17 @@ type AnswerQuizRequest struct {
57
  Answer int `json:"answer" binding:"required"`
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
60
  type (
61
  SavePersonalityAndPreferenceRequest struct {
62
  AccountID int64 `json:"-"`
 
57
  Answer int `json:"answer" binding:"required"`
58
  }
59
 
60
+ type (
61
+ CreateEmailVerificationRequest struct {
62
+ AccountID int64 `json:"-"`
63
+ }
64
+
65
+ ValidateEmailVerificationRequest struct {
66
+ AccountID int64 `json:"-"`
67
+ Token int64 `json:"token" validate:"required"`
68
+ }
69
+ )
70
+
71
  type (
72
  SavePersonalityAndPreferenceRequest struct {
73
  AccountID int64 `json:"-"`
space/space/repositories/email_repository.go ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package repositories
2
+
3
+ import (
4
+ "context"
5
+
6
+ "api.qobiltu.id/models"
7
+ "gorm.io/gorm"
8
+ )
9
+
10
+ type EmailRepository interface {
11
+ SaveEmailVerification(ctx context.Context, req *models.EmailVerification) (*models.EmailVerification, error)
12
+ GetEmailVerification(ctx context.Context, accountID int64, token int64) (*models.EmailVerification, error)
13
+ DeleteEmailVerification(ctx context.Context, id int64) error
14
+
15
+ // later, will relocate to account repository
16
+ GetAccountById(ctx context.Context, id int64) (*models.Account, error)
17
+ SaveAccount(ctx context.Context, req *models.Account) (*models.Account, error)
18
+ }
19
+
20
+ type emailRepository struct {
21
+ db *gorm.DB
22
+ }
23
+
24
+ func NewEmailRepository(db *gorm.DB) EmailRepository {
25
+ return &emailRepository{db: db}
26
+ }
27
+
28
+ func (r *emailRepository) SaveEmailVerification(ctx context.Context, req *models.EmailVerification) (*models.EmailVerification, error) {
29
+ err := r.db.WithContext(ctx).Save(req).Error
30
+ if err != nil {
31
+ return nil, err
32
+ }
33
+ return req, nil
34
+ }
35
+
36
+ func (r *emailRepository) GetEmailVerification(ctx context.Context, accountID int64, token int64) (*models.EmailVerification, error) {
37
+ var emailVerification models.EmailVerification
38
+ err := r.db.WithContext(ctx).Where("account_id = ? AND token = ?", accountID, token).First(&emailVerification).Error
39
+ if err != nil {
40
+ return nil, err
41
+ }
42
+
43
+ return &emailVerification, nil
44
+ }
45
+
46
+ func (r *emailRepository) DeleteEmailVerification(ctx context.Context, id int64) error {
47
+ return r.db.WithContext(ctx).Delete(&models.EmailVerification{}, id).Error
48
+ }
49
+
50
+ func (r *emailRepository) GetAccountById(ctx context.Context, id int64) (*models.Account, error) {
51
+ var account models.Account
52
+ err := r.db.WithContext(ctx).Where("id = ?", id).First(&account).Error
53
+ if err != nil {
54
+ return nil, err
55
+ }
56
+ return &account, nil
57
+ }
58
+
59
+ func (r *emailRepository) SaveAccount(ctx context.Context, req *models.Account) (*models.Account, error) {
60
+ err := r.db.WithContext(ctx).Save(req).Error
61
+ if err != nil {
62
+ return nil, err
63
+ }
64
+ return req, nil
65
+ }
space/space/router/email_route.go CHANGED
@@ -1,15 +1,13 @@
1
  package router
2
 
3
  import (
4
- EmailController "api.qobiltu.id/controller/email"
5
  "api.qobiltu.id/middleware"
6
- "github.com/gin-gonic/gin"
7
  )
8
 
9
- func EmailRoute(router *gin.Engine) {
10
- routerGroup := router.Group("/api/v1/email")
11
  {
12
- routerGroup.POST("/verify", middleware.AuthUser, EmailController.Verify)
13
- routerGroup.POST("/create-verification", middleware.AuthUser, EmailController.CreateVerification)
14
  }
15
  }
 
1
  package router
2
 
3
  import (
 
4
  "api.qobiltu.id/middleware"
 
5
  )
6
 
7
+ func (s *Server) EmailRoute() {
8
+ routerGroup := s.router.Group("/api/v1/email").Use(middleware.AuthUser)
9
  {
10
+ routerGroup.POST("/create-verification", s.emailController.CreateEmailVerification)
11
+ routerGroup.POST("/verify", s.emailController.Verify)
12
  }
13
  }
space/space/router/router.go CHANGED
@@ -10,12 +10,11 @@ func (s *Server) setupRoutes() {
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
13
- EmailRoute(s.router)
14
  OptionsRoute(s.router)
15
  AcademyRoute(s.router)
16
  QuizRoute(s.router)
17
 
18
- // another way to register routes
19
  s.CVRoute()
20
  s.MarriageReadinessProfileRoute()
21
  s.PartnerCriteriaRoute()
 
10
 
11
  AuthRoute(s.router)
12
  UserRoute(s.router)
 
13
  OptionsRoute(s.router)
14
  AcademyRoute(s.router)
15
  QuizRoute(s.router)
16
 
17
+ s.EmailRoute()
18
  s.CVRoute()
19
  s.MarriageReadinessProfileRoute()
20
  s.PartnerCriteriaRoute()
space/space/router/server.go CHANGED
@@ -2,6 +2,7 @@ package router
2
 
3
  import (
4
  cv_controller "api.qobiltu.id/controller/cv"
 
5
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
6
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
7
  "github.com/gin-gonic/gin"
@@ -9,12 +10,14 @@ import (
9
 
10
  type Server struct {
11
  router *gin.Engine
 
12
  cvController cv_controller.CVController
13
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
14
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
15
  }
16
 
17
  func NewServer(
 
18
  cvController cv_controller.CVController,
19
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
20
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
@@ -24,6 +27,7 @@ func NewServer(
24
  router.Use(gin.Recovery())
25
 
26
  server := &Server{
 
27
  cvController: cvController,
28
  marriageReadinessProfileController: marriageReadinessProfileController,
29
  partnerCriteriaController: partnerCriteriaController,
 
2
 
3
  import (
4
  cv_controller "api.qobiltu.id/controller/cv"
5
+ email_controller "api.qobiltu.id/controller/email"
6
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
7
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
8
  "github.com/gin-gonic/gin"
 
10
 
11
  type Server struct {
12
  router *gin.Engine
13
+ emailController email_controller.EmailController
14
  cvController cv_controller.CVController
15
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
16
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
17
  }
18
 
19
  func NewServer(
20
+ emailController email_controller.EmailController,
21
  cvController cv_controller.CVController,
22
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
23
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
 
27
  router.Use(gin.Recovery())
28
 
29
  server := &Server{
30
+ emailController: emailController,
31
  cvController: cvController,
32
  marriageReadinessProfileController: marriageReadinessProfileController,
33
  partnerCriteriaController: partnerCriteriaController,
space/space/services/email_service.go ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "context"
5
+ "strconv"
6
+ "time"
7
+
8
+ "api.qobiltu.id/config"
9
+ "api.qobiltu.id/pkg/worker"
10
+ "api.qobiltu.id/response"
11
+ "api.qobiltu.id/utils"
12
+ "github.com/hibiken/asynq"
13
+ uuid "github.com/satori/go.uuid"
14
+
15
+ "api.qobiltu.id/models"
16
+ "api.qobiltu.id/repositories"
17
+ )
18
+
19
+ type EmailService interface {
20
+ CreateEmailVerification(ctx context.Context, req *models.CreateEmailVerificationRequest) (*models.EmailVerification, error)
21
+ ValidateEmailVerification(ctx context.Context, req *models.ValidateEmailVerificationRequest) (*models.EmailVerification, error)
22
+ }
23
+
24
+ type emailService struct {
25
+ emailRepository repositories.EmailRepository
26
+ taskDistributor worker.TaskDistributor
27
+ }
28
+
29
+ func NewEmailService(emailRepository repositories.EmailRepository, taskDistributor worker.TaskDistributor) EmailService {
30
+ return &emailService{emailRepository: emailRepository, taskDistributor: taskDistributor}
31
+ }
32
+
33
+ func (s *emailService) CreateEmailVerification(ctx context.Context, req *models.CreateEmailVerificationRequest) (*models.EmailVerification, error) {
34
+ account, err := s.emailRepository.GetAccountById(ctx, req.AccountID)
35
+ if err != nil {
36
+ return nil, response.HandleGormError(err, "Internal Server Error")
37
+ }
38
+
39
+ token, err := utils.GenerateToken()
40
+ if err != nil {
41
+ return nil, models.Exception{
42
+ InternalServerError: true,
43
+ Message: "failed to generate token for email verification",
44
+ }
45
+ }
46
+
47
+ remainingTime := time.Duration(config.EMAIL_VERIFICATION_DURATION) * time.Minute
48
+ dueTime := time.Now().Add(remainingTime)
49
+
50
+ payload := models.EmailVerification{
51
+ AccountID: int64(account.Id),
52
+ Token: token,
53
+ UUID: uuid.NewV4(),
54
+ ExpiredAt: dueTime,
55
+ IsExpired: false,
56
+ }
57
+
58
+ emailVerification, err := s.emailRepository.SaveEmailVerification(ctx, &payload)
59
+ if err != nil {
60
+ return nil, response.HandleGormError(err, "Internal Server Error")
61
+ }
62
+
63
+ opts := []asynq.Option{
64
+ asynq.MaxRetry(worker.TaskSendVerifyEmailMaxRetry),
65
+ asynq.Queue(worker.Critical),
66
+ }
67
+
68
+ err = s.taskDistributor.DistributeTaskSendVerifyEmail(
69
+ context.Background(),
70
+ &worker.PayloadSendVerifyEmail{
71
+ EmailAddress: account.Email,
72
+ VerificationCode: strconv.Itoa(int(token)),
73
+ ExpirationInMinutes: int(remainingTime.Minutes()),
74
+ Subject: worker.TaskSendVerifyEmailSubject,
75
+ }, opts...)
76
+
77
+ if err != nil {
78
+ return nil, models.Exception{
79
+ InternalServerError: true,
80
+ Message: "failed to distribute task send verify email",
81
+ }
82
+ }
83
+
84
+ return emailVerification, nil
85
+ }
86
+
87
+ func (s *emailService) ValidateEmailVerification(ctx context.Context, req *models.ValidateEmailVerificationRequest) (*models.EmailVerification, error) {
88
+ emailVerification, err := s.emailRepository.GetEmailVerification(ctx, req.AccountID, int64(req.Token))
89
+ if err != nil {
90
+ return nil, response.HandleGormError(err, "Internal Server Error")
91
+ }
92
+
93
+ if emailVerification.ExpiredAt.Before(time.Now()) {
94
+ err = s.emailRepository.DeleteEmailVerification(ctx, emailVerification.ID)
95
+ if err != nil {
96
+ return nil, response.HandleGormError(err, "Internal Server Error")
97
+ }
98
+
99
+ return nil, models.Exception{
100
+ Unauthorized: true,
101
+ Message: "Token has expired!",
102
+ }
103
+ }
104
+
105
+ account, err := s.emailRepository.GetAccountById(ctx, emailVerification.AccountID)
106
+ if err != nil {
107
+ return nil, response.HandleGormError(err, "Internal Server Error")
108
+ }
109
+
110
+ account.IsEmailVerified = true
111
+
112
+ _, err = s.emailRepository.SaveAccount(ctx, account)
113
+ if err != nil {
114
+ return nil, response.HandleGormError(err, "Internal Server Error")
115
+ }
116
+
117
+ err = s.emailRepository.DeleteEmailVerification(ctx, emailVerification.ID)
118
+ if err != nil {
119
+ return nil, response.HandleGormError(err, "Internal Server Error")
120
+ }
121
+
122
+ return emailVerification, nil
123
+ }
space/space/space/controller/quiz/review_quiz_controller.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Review(c *gin.Context) {
13
+ reviewQuiz := services.ReviewQuizService{}
14
+ reviewQuizController := controller.Controller[any, models.Quiz, models.QuestionResponse]{
15
+ Service: &reviewQuiz.Service,
16
+ }
17
+ reviewQuizController.HeaderParse(c, func() {
18
+ quizId, _ := strconv.Atoi(c.Param("quiz_id"))
19
+ academyId, _ := strconv.Atoi(c.Param("academy_id"))
20
+ questionNo, _ := strconv.Atoi(c.Query("question_no"))
21
+ reviewQuizController.Service.Constructor.ID = uint(quizId)
22
+ reviewQuizController.Service.Constructor.AcademyID = uint(academyId)
23
+ reviewQuiz.Retrieve(reviewQuizController.AccountData.UserID, questionNo)
24
+ reviewQuizController.Response(c)
25
+ })
26
+ }
space/space/space/router/quiz_route.go CHANGED
@@ -12,6 +12,7 @@ func QuizRoute(router *gin.Engine) {
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)
16
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
17
  routerGroup.GET("result/", middleware.AuthUser, QuizController.Result)
 
12
  routerGroup.GET("/:academy_id/list", middleware.AuthUser, QuizController.List)
13
  routerGroup.POST("/:academy_id/:quiz_id/attempt", middleware.AuthUser, QuizController.Attempt)
14
  routerGroup.GET("/:academy_id/:quiz_id/question", middleware.AuthUser, QuizController.Question)
15
+ routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
16
  routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
17
  routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
18
  routerGroup.GET("result/", middleware.AuthUser, QuizController.Result)
space/space/space/services/academy_quiz_question_service.go CHANGED
@@ -34,6 +34,8 @@ func (s *QuestionQuizService) Retrieve(userID uint, questionNo int) {
34
  s.Exception.Message = "There is no Answer Option with given QuestionId!"
35
  return
36
  }
 
 
37
  s.Result = models.QuestionResponse{
38
  Question: questionRepo.Result,
39
  Answer: answerOptionRepo.Result,
 
34
  s.Exception.Message = "There is no Answer Option with given QuestionId!"
35
  return
36
  }
37
+ questionRepo.Result.Review = "SECRET"
38
+ questionRepo.Result.CorrectAnswer = 0
39
  s.Result = models.QuestionResponse{
40
  Question: questionRepo.Result,
41
  Answer: answerOptionRepo.Result,
space/space/space/services/academy_quiz_review_service.go ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "errors"
5
+
6
+ "api.qobiltu.id/models"
7
+ "api.qobiltu.id/repositories"
8
+ )
9
+
10
+ type ReviewQuizService struct {
11
+ Service[models.Quiz, models.QuestionResponse]
12
+ }
13
+
14
+ func (s *ReviewQuizService) Retrieve(userID uint, questionNo int) {
15
+ latestAttemptRepo := repositories.GetUserLastAttempt(userID, s.Constructor.ID)
16
+ s.Error = latestAttemptRepo.RowsError
17
+ if latestAttemptRepo.NoRecord {
18
+ s.Exception.DataNotFound = true
19
+ s.Exception.Message = "There is no attempt data with given user ID!"
20
+ return
21
+ }
22
+ quizRepo := repositories.GetQuizbyId(s.Constructor.ID)
23
+ s.Error = errors.Join(s.Error, quizRepo.RowsError)
24
+ if latestAttemptRepo.Result.Score >= float64(quizRepo.Result.MinScore) {
25
+ questionRepo := repositories.GetQuestionByOrder(s.Constructor.ID, questionNo)
26
+ s.Error = questionRepo.RowsError
27
+ if questionRepo.NoRecord {
28
+ s.Exception.DataNotFound = true
29
+ s.Exception.Message = "There is no quiz with given academy!"
30
+ return
31
+ }
32
+ answerRepo := repositories.GetUserAnswerByAttemptQuestionId(latestAttemptRepo.Result.ID, questionRepo.Result.ID)
33
+ if answerRepo.NoRecord {
34
+ s.Exception.DataNotFound = true
35
+ s.Exception.Message = "There is no Answer with given AttemptId!"
36
+ return
37
+ }
38
+ answerOptionRepo := repositories.GetAnswerByQuestionId(questionRepo.Result.ID)
39
+ if answerOptionRepo.NoRecord {
40
+ s.Exception.DataNotFound = true
41
+ s.Exception.Message = "There is no Answer Option with given QuestionId!"
42
+ return
43
+ }
44
+ s.Result = models.QuestionResponse{
45
+ Question: questionRepo.Result,
46
+ Answer: answerOptionRepo.Result,
47
+ UserAnswer: int(answerRepo.Result.SelectedAnswer),
48
+ }
49
+ } else {
50
+ s.Exception.Forbidden = true
51
+ s.Exception.Message = "You have to passed the exam to review the quiz! "
52
+ return
53
+ }
54
+
55
+ return
56
+ }
space/space/space/services/academy_quiz_service.go CHANGED
@@ -2,7 +2,6 @@ package services
2
 
3
  import (
4
  "errors"
5
- "fmt"
6
  "time"
7
 
8
  "api.qobiltu.id/models"
@@ -131,7 +130,8 @@ func (s *AttemptQuizService) Create(userID uint) {
131
  if latestAttemptRepo.Result.Score < float64(quizRepo.Result.MinScore) {
132
  Attempt(s, quizRepo, userID)
133
  } else {
134
- s.Result = latestAttemptRepo.Result
 
135
  return
136
  }
137
  } else {
@@ -142,10 +142,7 @@ func (s *AttemptQuizService) Create(userID uint) {
142
  }
143
 
144
  func (s *SubmitQuizService) Create() {
145
- fmt.Println(s.Constructor.ID)
146
- fmt.Println(s.Constructor.AccountID)
147
  quizAttemptRepo := repositories.GetAttemptByIdandUserId(s.Constructor.ID, s.Constructor.AccountID)
148
- fmt.Println(quizAttemptRepo.Result)
149
  if quizAttemptRepo.NoRecord {
150
  s.Exception.DataNotFound = true
151
  s.Exception.Message = "There is no quiz attempt with given user!"
 
2
 
3
  import (
4
  "errors"
 
5
  "time"
6
 
7
  "api.qobiltu.id/models"
 
130
  if latestAttemptRepo.Result.Score < float64(quizRepo.Result.MinScore) {
131
  Attempt(s, quizRepo, userID)
132
  } else {
133
+ s.Exception.Forbidden = true
134
+ s.Exception.Message = "You're alread passed the quiz, you don't have to re-attempt!"
135
  return
136
  }
137
  } else {
 
142
  }
143
 
144
  func (s *SubmitQuizService) Create() {
 
 
145
  quizAttemptRepo := repositories.GetAttemptByIdandUserId(s.Constructor.ID, s.Constructor.AccountID)
 
146
  if quizAttemptRepo.NoRecord {
147
  s.Exception.DataNotFound = true
148
  s.Exception.Message = "There is no quiz attempt with given user!"
space/space/space/space/main.go CHANGED
@@ -7,7 +7,6 @@ import (
7
 
8
  "api.qobiltu.id/config"
9
  cv_controller "api.qobiltu.id/controller/cv"
10
- health_check_controller "api.qobiltu.id/controller/health_check"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
  "api.qobiltu.id/mail"
@@ -55,10 +54,6 @@ func main() {
55
  worker.AsyncTaskDistributor = taskDistributor
56
 
57
  // setup repo, service, and controller
58
- healthCheckRepository := repositories.NewHealthCheckRepository(config.DB)
59
- healthCheckService := services.NewHealthCheckService(healthCheckRepository)
60
- healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
61
-
62
  cvRepository := repositories.NewCVRepository(config.DB)
63
  cvService := services.NewCVService(cvRepository, localStorage, validator)
64
  cvController := cv_controller.NewCVController(cvService)
@@ -78,7 +73,6 @@ func main() {
78
 
79
  // create server
80
  s, err := router.NewServer(
81
- healthCheckController,
82
  cvController,
83
  marriageReadinessProfileController,
84
  partnerCriteriaController,
 
7
 
8
  "api.qobiltu.id/config"
9
  cv_controller "api.qobiltu.id/controller/cv"
 
10
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
11
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
12
  "api.qobiltu.id/mail"
 
54
  worker.AsyncTaskDistributor = taskDistributor
55
 
56
  // setup repo, service, and controller
 
 
 
 
57
  cvRepository := repositories.NewCVRepository(config.DB)
58
  cvService := services.NewCVService(cvRepository, localStorage, validator)
59
  cvController := cv_controller.NewCVController(cvService)
 
73
 
74
  // create server
75
  s, err := router.NewServer(
 
76
  cvController,
77
  marriageReadinessProfileController,
78
  partnerCriteriaController,
space/space/space/space/models/database_orm_model.go CHANGED
@@ -78,8 +78,10 @@ type Academy struct {
78
  Slug string `json:"slug" gorm:"uniqueIndex" `
79
  TotalMaterial int `json:"total_material"`
80
  CompletedMaterial int `json:"completed_material"`
 
 
81
  IsCompletedRead bool `json:"is_read"`
82
- IsPassedExam bool `json:"is_exam"`
83
  Description string `json:"description"`
84
  }
85
 
@@ -189,6 +191,7 @@ type QuizResult struct {
189
  TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
190
  CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
191
  AverageScore float64 `gorm:"column:average_score" json:"average_score"`
 
192
  }
193
 
194
  type (
@@ -396,7 +399,8 @@ type (
396
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
397
 
398
  // Kriteria Umum
399
- ExpectedAgeLimit *int `gorm:"column:expected_age_limit" json:"expected_age_limit"` // batas usia pasangan yang diharapkan
 
400
  ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
401
  AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
402
  PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
@@ -405,11 +409,12 @@ type (
405
  PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
406
 
407
  // Kriteria Fisik
408
- ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
409
- ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
410
- ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
411
- ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
412
- ExpectedHeight *int `gorm:"column:expected_height" json:"expected_height"` // tinggi badan yang diharapkan
 
413
 
414
  // Pendidikan & Pekerjaan
415
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
 
78
  Slug string `json:"slug" gorm:"uniqueIndex" `
79
  TotalMaterial int `json:"total_material"`
80
  CompletedMaterial int `json:"completed_material"`
81
+ TotalQuiz int `json:"total_quiz"`
82
+ PassedQuiz int `json:"passed_quiz"`
83
  IsCompletedRead bool `json:"is_read"`
84
+ IsPassedExam bool `json:"is_passed_exam"`
85
  Description string `json:"description"`
86
  }
87
 
 
191
  TotalQuestions int `gorm:"column:total_questions" json:"total_questions"`
192
  CorrectAnswers int `gorm:"column:correct_answers" json:"correct_answers"`
193
  AverageScore float64 `gorm:"column:average_score" json:"average_score"`
194
+ IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
195
  }
196
 
197
  type (
 
399
  Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
400
 
401
  // Kriteria Umum
402
+ ExpecteMinAgeLimit *int `gorm:"column:expected_min_age_limit" json:"expected_min_age_limit"` // batas usia pasangan yang diharapkan
403
+ ExpecteMaxAgeLimit *int `gorm:"column:expected_max_age_limit" json:"expected_max_age_limit"` // batas usia pasangan yang diharapkan
404
  ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
405
  AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
406
  PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
 
409
  PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
410
 
411
  // Kriteria Fisik
412
+ ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
413
+ ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
414
+ ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
415
+ ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
416
+ ExpectedMinHeightLimit *int `gorm:"column:expected_min_height_limit" json:"expected_min_height_limit"` // tinggi badan yang diharapkan
417
+ ExpectedMaxHeightLimit *int `gorm:"column:expected_max_height_limit" json:"expected_max_height_limit"` // tinggi badan yang diharapkan
418
 
419
  // Pendidikan & Pekerjaan
420
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
space/space/space/space/models/request_model.go CHANGED
@@ -336,7 +336,8 @@ type (
336
  SavePartnerCriteriaRequest struct {
337
  AccountID int64 `json:"account_id"`
338
 
339
- ExpectedAgeLimit *int `gorm:"column:expected_age_limit" json:"expected_age_limit"` // batas usia pasangan yang diharapkan
 
340
  ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
341
  AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
342
  PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
@@ -345,11 +346,12 @@ type (
345
  PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
346
 
347
  // Kriteria Fisik
348
- ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
349
- ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
350
- ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
351
- ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
352
- ExpectedHeight *int `gorm:"column:expected_height" json:"expected_height"` // tinggi badan yang diharapkan
 
353
 
354
  // Pendidikan & Pekerjaan
355
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
 
336
  SavePartnerCriteriaRequest struct {
337
  AccountID int64 `json:"account_id"`
338
 
339
+ ExpecteMinAgeLimit *int `gorm:"column:expected_min_age_limit" json:"expected_min_age_limit"` // batas usia pasangan yang diharapkan
340
+ ExpecteMaxAgeLimit *int `gorm:"column:expected_max_age_limit" json:"expected_max_age_limit"` // batas usia pasangan yang diharapkan
341
  ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
342
  AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
343
  PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
 
346
  PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
347
 
348
  // Kriteria Fisik
349
+ ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
350
+ ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
351
+ ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
352
+ ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
353
+ ExpectedMinHeightLimit *int `gorm:"column:expected_min_height_limit" json:"expected_min_height_limit"` // tinggi badan yang diharapkan
354
+ ExpectedMaxHeightLimit *int `gorm:"column:expected_max_height_limit" json:"expected_max_height_limit"` // tinggi badan yang diharapkan
355
 
356
  // Pendidikan & Pekerjaan
357
  ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
space/space/space/space/router/router.go CHANGED
@@ -16,7 +16,6 @@ func (s *Server) setupRoutes() {
16
  QuizRoute(s.router)
17
 
18
  // another way to register routes
19
- s.HealthCheckRoute()
20
  s.CVRoute()
21
  s.MarriageReadinessProfileRoute()
22
  s.PartnerCriteriaRoute()
 
16
  QuizRoute(s.router)
17
 
18
  // another way to register routes
 
19
  s.CVRoute()
20
  s.MarriageReadinessProfileRoute()
21
  s.PartnerCriteriaRoute()
space/space/space/space/router/server.go CHANGED
@@ -2,7 +2,6 @@ package router
2
 
3
  import (
4
  cv_controller "api.qobiltu.id/controller/cv"
5
- health_check_controller "api.qobiltu.id/controller/health_check"
6
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
7
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
8
  "github.com/gin-gonic/gin"
@@ -10,14 +9,12 @@ import (
10
 
11
  type Server struct {
12
  router *gin.Engine
13
- healthCheckController health_check_controller.HealthCheckController
14
  cvController cv_controller.CVController
15
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
16
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
17
  }
18
 
19
  func NewServer(
20
- healthCheckController health_check_controller.HealthCheckController,
21
  cvController cv_controller.CVController,
22
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
23
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
@@ -27,7 +24,6 @@ func NewServer(
27
  router.Use(gin.Recovery())
28
 
29
  server := &Server{
30
- healthCheckController: healthCheckController,
31
  cvController: cvController,
32
  marriageReadinessProfileController: marriageReadinessProfileController,
33
  partnerCriteriaController: partnerCriteriaController,
 
2
 
3
  import (
4
  cv_controller "api.qobiltu.id/controller/cv"
 
5
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
6
  partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
7
  "github.com/gin-gonic/gin"
 
9
 
10
  type Server struct {
11
  router *gin.Engine
 
12
  cvController cv_controller.CVController
13
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
14
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
15
  }
16
 
17
  func NewServer(
 
18
  cvController cv_controller.CVController,
19
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
20
  partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
 
24
  router.Use(gin.Recovery())
25
 
26
  server := &Server{
 
27
  cvController: cvController,
28
  marriageReadinessProfileController: marriageReadinessProfileController,
29
  partnerCriteriaController: partnerCriteriaController,
space/space/space/space/services/partner_criteria_service.go CHANGED
@@ -42,7 +42,8 @@ func (s *partnerCriteriaService) SavePartnerCriteria(ctx context.Context, req *m
42
 
43
  partnerCriteria.AccountID = req.AccountID
44
 
45
- utils.AssignIfNotNil(&partnerCriteria.ExpectedAgeLimit, req.ExpectedAgeLimit)
 
46
  utils.AssignIfNotNil(&partnerCriteria.ExpectedDomicile, req.ExpectedDomicile)
47
  utils.AssignIfNotNil(&partnerCriteria.AcceptedMaritalStatus, req.AcceptedMaritalStatus)
48
  utils.AssignIfNotNil(&partnerCriteria.PartnerFamilyReligion, req.PartnerFamilyReligion)
@@ -54,7 +55,8 @@ func (s *partnerCriteriaService) SavePartnerCriteria(ctx context.Context, req *m
54
  utils.AssignIfNotNil(&partnerCriteria.ExpectedSkinColors, req.ExpectedSkinColors)
55
  utils.AssignIfNotNil(&partnerCriteria.ExpectedHairTypes, req.ExpectedHairTypes)
56
  utils.AssignIfNotNil(&partnerCriteria.ExpectedHairThickness, req.ExpectedHairThickness)
57
- utils.AssignIfNotNil(&partnerCriteria.ExpectedHeight, req.ExpectedHeight)
 
58
 
59
  utils.AssignIfNotNil(&partnerCriteria.ExpectedPartnerIncome, req.ExpectedPartnerIncome)
60
  utils.AssignIfNotNil(&partnerCriteria.ExpectedIncomeSources, req.ExpectedIncomeSources)
 
42
 
43
  partnerCriteria.AccountID = req.AccountID
44
 
45
+ utils.AssignIfNotNil(&partnerCriteria.ExpecteMinAgeLimit, req.ExpecteMinAgeLimit)
46
+ utils.AssignIfNotNil(&partnerCriteria.ExpecteMaxAgeLimit, req.ExpecteMaxAgeLimit)
47
  utils.AssignIfNotNil(&partnerCriteria.ExpectedDomicile, req.ExpectedDomicile)
48
  utils.AssignIfNotNil(&partnerCriteria.AcceptedMaritalStatus, req.AcceptedMaritalStatus)
49
  utils.AssignIfNotNil(&partnerCriteria.PartnerFamilyReligion, req.PartnerFamilyReligion)
 
55
  utils.AssignIfNotNil(&partnerCriteria.ExpectedSkinColors, req.ExpectedSkinColors)
56
  utils.AssignIfNotNil(&partnerCriteria.ExpectedHairTypes, req.ExpectedHairTypes)
57
  utils.AssignIfNotNil(&partnerCriteria.ExpectedHairThickness, req.ExpectedHairThickness)
58
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedMinHeightLimit, req.ExpectedMinHeightLimit)
59
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedMaxHeightLimit, req.ExpectedMaxHeightLimit)
60
 
61
  utils.AssignIfNotNil(&partnerCriteria.ExpectedPartnerIncome, req.ExpectedPartnerIncome)
62
  utils.AssignIfNotNil(&partnerCriteria.ExpectedIncomeSources, req.ExpectedIncomeSources)
space/space/space/space/space/pkg/validation/validation.go CHANGED
@@ -1,174 +1,35 @@
1
  package validation
2
 
3
  import (
4
- "errors"
5
- "fmt"
6
- "reflect"
7
- "strings"
8
 
9
- "github.com/go-playground/locales/en"
10
- "github.com/go-playground/locales/id"
11
- ut "github.com/go-playground/universal-translator"
12
- v10 "github.com/go-playground/validator/v10"
13
- entranslations "github.com/go-playground/validator/v10/translations/en"
14
- idtranslations "github.com/go-playground/validator/v10/translations/id"
15
  )
16
 
17
- // Constants for supported locales
18
- const (
19
- LocaleID = "id"
20
- LocaleEN = "en"
21
- )
22
-
23
- // ErrorMessage represents a validation error message
24
  type ErrorMessage struct {
25
  Field string `json:"field"`
26
  Message string `json:"-"`
27
  }
28
 
29
- type validator struct {
30
- validate *v10.Validate
31
- translator ut.Translator
32
- }
33
-
34
- // validatorInstance adalah instance global dari validator.
35
- var validatorInstance *validator
36
-
37
- // New creates a new validation instance with the specified locale
38
- // dan menginisialisasi instance global validatorInstance.
39
- func New(locale string) error {
40
- v := &validator{}
41
- parsedLocale := parseLocale(locale)
42
-
43
- uni := ut.New(en.New(), id.New(), en.New())
44
- translator, found := uni.GetTranslator(parsedLocale)
45
- if !found {
46
- return fmt.Errorf("translator not found for locale: %s", parsedLocale)
47
- }
48
-
49
- validate := v10.New()
50
-
51
- if err := setupValidations(validate); err != nil {
52
- return fmt.Errorf("failed to setup validations: %w", err)
53
- }
54
-
55
- if err := setupTranslations(validate, translator, parsedLocale); err != nil {
56
- return fmt.Errorf("failed to setup translations for locale %s: %w", parsedLocale, err)
57
- }
58
-
59
- v.validate = validate
60
- v.translator = translator
61
-
62
- validatorInstance = v // Inisialisasi instance global
63
- return nil
64
- }
65
-
66
- func parseLocale(locale string) string {
67
- switch strings.ToLower(locale) {
68
- case "id":
69
- return LocaleID
70
- case "en":
71
- return LocaleEN
72
- default:
73
- return LocaleID // Default to Indonesian
74
- }
75
- }
76
-
77
- // setupValidations configures custom validation rules.
78
- func setupValidations(validate *v10.Validate) error {
79
- rules := NewValidatorRules(&InMemoryOptionSource{})
80
- if err := rules.RegisterAllCustomRules(validate); err != nil {
81
- return err
82
- }
83
-
84
- return nil
85
- }
86
 
87
- // setupTranslations configures translations for validation messages.
88
- func setupTranslations(validate *v10.Validate, translator ut.Translator, locale string) error {
89
- // Register default translations based on locale
90
- if err := registerDefaultTranslations(validate, translator, locale); err != nil {
91
- return fmt.Errorf("failed to register default translations for locale %s: %w", locale, err)
92
- }
93
-
94
- // Register custom password validation translation
95
- err := validate.RegisterTranslation("password", translator,
96
- func(ut ut.Translator) error {
97
- return ut.Add("password", "harus mengandung minimal 8 karakter, huruf besar, huruf kecil, dan angka.", true)
98
- },
99
- func(ut ut.Translator, fe v10.FieldError) string {
100
- translated, err := ut.T(fe.Tag(), fe.Field())
101
- if err != nil {
102
- return fe.Field() + " is invalid"
103
- }
104
- return translated
105
- },
106
- )
107
  if err != nil {
108
- return fmt.Errorf("failed to register password translation: %w", err)
109
- }
110
-
111
- return nil
112
- }
113
-
114
- // registerDefaultTranslations sets up default translations for the specified locale.
115
- func registerDefaultTranslations(validate *v10.Validate, translator ut.Translator, locale string) error {
116
- switch locale {
117
- case LocaleID:
118
- return idtranslations.RegisterDefaultTranslations(validate, translator)
119
- case LocaleEN:
120
- return entranslations.RegisterDefaultTranslations(validate, translator)
121
- default:
122
- // Fallback to English if the locale is not supported
123
- return entranslations.RegisterDefaultTranslations(validate, translator)
124
  }
125
- }
126
-
127
- // Validate validates a struct using the global validator instance
128
- // and returns a slice of ErrorMessage.
129
- func Validate(s any) []ErrorMessage {
130
- if validatorInstance == nil {
131
- return []ErrorMessage{{Field: "", Message: "Validator belum diinisialisasi. Panggil validation.New() terlebih dahulu."}}
132
- }
133
-
134
- err := validatorInstance.validate.Struct(s)
135
- if err != nil {
136
- return TranslateError(err)
137
- }
138
-
139
- return nil
140
- }
141
-
142
- // TranslateError takes a validation error and translates it using the global translator.
143
- func TranslateError(err error) []ErrorMessage {
144
- if validatorInstance == nil {
145
- return nil
146
- }
147
-
148
- var validationErrors v10.ValidationErrors
149
- if !errors.As(err, &validationErrors) {
150
- return nil
151
- }
152
-
153
- var errorMessages []ErrorMessage
154
-
155
- for _, e := range validationErrors {
156
- fieldLabel := e.Field()
157
-
158
- if e.Kind() == reflect.Ptr {
159
- continue
160
- }
161
 
162
- msg, err := validatorInstance.translator.T(e.Tag(), fieldLabel)
163
- if err != nil {
164
- msg = fieldLabel + " is Invalid"
165
- }
166
 
167
- errorMessages = append(errorMessages, ErrorMessage{
168
- Field: e.Tag(),
169
- Message: msg,
170
- })
171
  }
172
 
173
- return errorMessages
174
  }
 
1
  package validation
2
 
3
  import (
4
+ "context"
5
+ "time"
 
 
6
 
7
+ "gorm.io/gorm"
 
 
 
 
 
8
  )
9
 
 
 
 
 
 
 
 
10
  type ErrorMessage struct {
11
  Field string `json:"field"`
12
  Message string `json:"-"`
13
  }
14
 
15
+ func New(db *gorm.DB) (*Validator, error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ // Create source with 5 minute cache expiry
18
+ expiry := 5 * time.Minute
19
+ dbSource, err := NewDBOptionSource(db, expiry)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  if err != nil {
21
+ return nil, err
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ // Start background refresh every 10 minutes
25
+ ctx := context.Background()
26
+ dbSource.StartAutoRefresh(ctx, 10*time.Minute)
 
27
 
28
+ // create validator
29
+ validator := NewValidator(dbSource)
30
+ if err := validator.RegisterAllCustomRules(); err != nil {
31
+ return nil, err
32
  }
33
 
34
+ return validator, nil
35
  }
space/space/space/space/space/response/validation.go CHANGED
@@ -3,12 +3,29 @@ package response
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/pkg/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
  }
 
3
  import (
4
  "api.qobiltu.id/models"
5
  "api.qobiltu.id/pkg/validation"
6
+ "github.com/go-playground/validator/v10"
7
  )
8
 
9
+ func HandleValidationError(err error) error {
10
+ validationErrors, ok := err.(validator.ValidationErrors)
11
+ if !ok {
12
+ return models.Exception{
13
+ ValidationError: true,
14
+ Message: "Validation failed",
15
+ }
16
+ }
17
+
18
+ validationErrorMessages := make([]validation.ErrorMessage, len(validationErrors))
19
+ for i, err := range validationErrors {
20
+ validationErrorMessages[i] = validation.ErrorMessage{
21
+ Field: err.Field(),
22
+ Message: err.Error(),
23
+ }
24
+ }
25
+
26
  return models.Exception{
27
  ValidationError: true,
28
  Message: "Validation failed",
29
+ ValidationErrorFields: validationErrorMessages,
30
  }
31
  }
space/space/space/space/space/services/marriage_readiness_profile_service.go CHANGED
@@ -19,14 +19,15 @@ type MarriageReadinessProfileService interface {
19
 
20
  type marriageReadinessProfileService struct {
21
  marriageReadinessProfileRepository repositories.MarriageReadinessProfileRepository
 
22
  }
23
 
24
- func NewMarriageReadinessProfileService(marriageReadinessProfileRepository repositories.MarriageReadinessProfileRepository) MarriageReadinessProfileService {
25
- return &marriageReadinessProfileService{marriageReadinessProfileRepository: marriageReadinessProfileRepository}
26
  }
27
 
28
  func (s *marriageReadinessProfileService) SaveMarriageReadinessProfile(ctx context.Context, req *models.SaveMarriageReadinessProfileRequest) (*models.MarriageReadinessProfile, error) {
29
- if err := validation.Validate(req); err != nil {
30
  return nil, response.HandleValidationError(err)
31
  }
32
 
 
19
 
20
  type marriageReadinessProfileService struct {
21
  marriageReadinessProfileRepository repositories.MarriageReadinessProfileRepository
22
+ validator *validation.Validator
23
  }
24
 
25
+ func NewMarriageReadinessProfileService(marriageReadinessProfileRepository repositories.MarriageReadinessProfileRepository, validator *validation.Validator) MarriageReadinessProfileService {
26
+ return &marriageReadinessProfileService{marriageReadinessProfileRepository: marriageReadinessProfileRepository, validator: validator}
27
  }
28
 
29
  func (s *marriageReadinessProfileService) SaveMarriageReadinessProfile(ctx context.Context, req *models.SaveMarriageReadinessProfileRequest) (*models.MarriageReadinessProfile, error) {
30
+ if err := s.validator.Validate(req); err != nil {
31
  return nil, response.HandleValidationError(err)
32
  }
33
 
space/space/space/space/space/space/controller/partner_criteria/partner_criteria_controller.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package partner_criteria_controller
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "api.qobiltu.id/middleware"
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/response"
9
+ "api.qobiltu.id/services"
10
+ "github.com/gin-gonic/gin"
11
+ )
12
+
13
+ type PartnerCriteriaController interface {
14
+ SavePartnerCriteria(ctx *gin.Context)
15
+ GetPartnerCriteria(ctx *gin.Context)
16
+ }
17
+
18
+ type partnerCriteriaController struct {
19
+ partnerCriteriaService services.PartnerCriteriaService
20
+ }
21
+
22
+ func NewPartnerCriteriaController(partnerCriteriaService services.PartnerCriteriaService) PartnerCriteriaController {
23
+ return &partnerCriteriaController{
24
+ partnerCriteriaService: partnerCriteriaService,
25
+ }
26
+ }
27
+
28
+ func (c *partnerCriteriaController) SavePartnerCriteria(ctx *gin.Context) {
29
+ var req models.SavePartnerCriteriaRequest
30
+ if err := ctx.ShouldBindJSON(&req); err != nil {
31
+ response.HandleError(ctx, models.Exception{
32
+ Message: "Invalid body request",
33
+ BadRequest: true,
34
+ Err: err,
35
+ })
36
+ return
37
+ }
38
+
39
+ accountData := middleware.GetAccountData(ctx)
40
+ req.AccountID = int64(accountData.UserID)
41
+
42
+ res, err := c.partnerCriteriaService.SavePartnerCriteria(ctx, &req)
43
+ if err != nil {
44
+ response.HandleError(ctx, err)
45
+ return
46
+ }
47
+
48
+ response.HandleSuccess(ctx, http.StatusOK, "Partner criteria saved", res, nil)
49
+ }
50
+
51
+ func (c *partnerCriteriaController) GetPartnerCriteria(ctx *gin.Context) {
52
+ accountData := middleware.GetAccountData(ctx)
53
+ accountID := int64(accountData.UserID)
54
+
55
+ req := models.GetPartnerCriteriaRequest{
56
+ AccountID: accountID,
57
+ }
58
+
59
+ res, err := c.partnerCriteriaService.GetPartnerCriteria(ctx, &req)
60
+ if err != nil {
61
+ response.HandleError(ctx, err)
62
+ return
63
+ }
64
+
65
+ response.HandleSuccess(ctx, http.StatusOK, "Get partner criteria success", res, nil)
66
+ }
space/space/space/space/space/space/main.go CHANGED
@@ -9,6 +9,7 @@ import (
9
  cv_controller "api.qobiltu.id/controller/cv"
10
  health_check_controller "api.qobiltu.id/controller/health_check"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
 
12
  "api.qobiltu.id/mail"
13
  "api.qobiltu.id/pkg/storage"
14
  "api.qobiltu.id/pkg/validation"
@@ -23,7 +24,7 @@ import (
23
  func main() {
24
 
25
  // setup validation
26
- err := validation.New(validation.LocaleID)
27
  utils.FatalIfErr("failed to setup validator", err)
28
 
29
  // setup storage
@@ -59,13 +60,17 @@ func main() {
59
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
60
 
61
  cvRepository := repositories.NewCVRepository(config.DB)
62
- cvService := services.NewCVService(cvRepository, localStorage)
63
  cvController := cv_controller.NewCVController(cvService)
64
 
65
  marriageReadinessProfileRepository := repositories.NewMarriageReadinessProfileRepository(config.DB)
66
- marriageReadinessProfileService := services.NewMarriageReadinessProfileService(marriageReadinessProfileRepository)
67
  marriageReadinessProfileController := marriage_readiness_profile_controller.NewMarriageReadinessProfileController(marriageReadinessProfileService)
68
 
 
 
 
 
69
  // start task processor
70
  err = taskProcessor.Start()
71
  utils.FatalIfErr("failed to start task processor", err)
@@ -76,6 +81,7 @@ func main() {
76
  healthCheckController,
77
  cvController,
78
  marriageReadinessProfileController,
 
79
  )
80
  utils.FatalIfErr("failed to create server", err)
81
 
 
9
  cv_controller "api.qobiltu.id/controller/cv"
10
  health_check_controller "api.qobiltu.id/controller/health_check"
11
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
12
+ partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
13
  "api.qobiltu.id/mail"
14
  "api.qobiltu.id/pkg/storage"
15
  "api.qobiltu.id/pkg/validation"
 
24
  func main() {
25
 
26
  // setup validation
27
+ validator, err := validation.New(config.DB)
28
  utils.FatalIfErr("failed to setup validator", err)
29
 
30
  // setup storage
 
60
  healthCheckController := health_check_controller.NewHealthCheckController(healthCheckService)
61
 
62
  cvRepository := repositories.NewCVRepository(config.DB)
63
+ cvService := services.NewCVService(cvRepository, localStorage, validator)
64
  cvController := cv_controller.NewCVController(cvService)
65
 
66
  marriageReadinessProfileRepository := repositories.NewMarriageReadinessProfileRepository(config.DB)
67
+ marriageReadinessProfileService := services.NewMarriageReadinessProfileService(marriageReadinessProfileRepository, validator)
68
  marriageReadinessProfileController := marriage_readiness_profile_controller.NewMarriageReadinessProfileController(marriageReadinessProfileService)
69
 
70
+ partnerCriteriaRepository := repositories.NewPartnerCriteriaRepository(config.DB)
71
+ partnerCriteriaService := services.NewPartnerCriteriaService(partnerCriteriaRepository, validator)
72
+ partnerCriteriaController := partner_criteria_controller.NewPartnerCriteriaController(partnerCriteriaService)
73
+
74
  // start task processor
75
  err = taskProcessor.Start()
76
  utils.FatalIfErr("failed to start task processor", err)
 
81
  healthCheckController,
82
  cvController,
83
  marriageReadinessProfileController,
84
+ partnerCriteriaController,
85
  )
86
  utils.FatalIfErr("failed to create server", err)
87
 
space/space/space/space/space/space/models/database_orm_model.go CHANGED
@@ -248,26 +248,27 @@ type (
248
  }
249
 
250
  WorshipAndReligiousUnderstandingCV struct {
251
- ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
252
- AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
253
- Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
254
- ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
255
- CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
256
- TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
257
- DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
258
- QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
259
- QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
260
- DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
261
- AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
262
- HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
263
- ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
264
- OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
265
- OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
266
- OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
267
- WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
268
- FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
269
- CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
270
- UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
 
271
  FieldCounter
272
  }
273
 
@@ -388,6 +389,39 @@ type (
388
  }
389
  )
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  // Gorm table name settings
392
  func (Account) TableName() string { return "account" }
393
  func (AccountDetails) TableName() string { return "account_details" }
 
248
  }
249
 
250
  WorshipAndReligiousUnderstandingCV struct {
251
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
252
+ AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
253
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
254
+ ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
255
+ CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
256
+ TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
257
+ DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
258
+ QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
259
+ QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
260
+ WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // kajian_yang_diikuti_dalam_sepekan
261
+ DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
262
+ AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
263
+ HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
264
+ ListeningToMusic *string `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
265
+ OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
266
+ OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
267
+ OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
268
+ WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
269
+ FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
270
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
271
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
272
  FieldCounter
273
  }
274
 
 
389
  }
390
  )
391
 
392
+ type (
393
+ PartnerCriteria struct {
394
+ ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
395
+ AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id"`
396
+ Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
397
+
398
+ // Kriteria Umum
399
+ ExpectedAgeLimit *int `gorm:"column:expected_age_limit" json:"expected_age_limit"` // batas usia pasangan yang diharapkan
400
+ ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
401
+ AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
402
+ PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
403
+ PartnerWeeklyReligiousStudyFrequency *string `gorm:"column:partner_weekly_religious_study_frequency" json:"partner_weekly_religious_study_frequency"` // rata-rata jumlah kajian yang diikuti pasangan per pekan
404
+ PartnerMonthlySpendingEstimate *string `gorm:"column:partner_monthly_spending_estimate" json:"partner_monthly_spending_estimate"` // estimasi pengeluaran pasangan per bulan
405
+ PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
406
+
407
+ // Kriteria Fisik
408
+ ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
409
+ ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
410
+ ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
411
+ ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
412
+ ExpectedHeight *int `gorm:"column:expected_height" json:"expected_height"` // tinggi badan yang diharapkan
413
+
414
+ // Pendidikan & Pekerjaan
415
+ ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
416
+ ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
417
+ ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
418
+ ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
419
+
420
+ CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
421
+ UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
422
+ }
423
+ )
424
+
425
  // Gorm table name settings
426
  func (Account) TableName() string { return "account" }
427
  func (AccountDetails) TableName() string { return "account_details" }
space/space/space/space/space/space/models/request_model.go CHANGED
@@ -74,7 +74,7 @@ type (
74
  FavoriteFoodAndDrinks *string `json:"favorite_food_and_drinks"` // makanan dan minuman favorit
75
  CanCook *bool `json:"can_cook"` // bisa memasak
76
  TypesOfDishesCooked *string `json:"types_of_dishes_cooked"` // jenis masakan yang bisa dimasak
77
- MonthlyExpenses *string `json:"monthly_expenses" validate:"monthly_expenses"` // pengeluaran per bulan
78
  }
79
 
80
  GetPersonalityAndPreferenceRequest struct {
@@ -84,11 +84,11 @@ type (
84
  CreateFamilyMemberRequest struct {
85
  AccountID int64 `json:"-"`
86
 
87
- Role *string `json:"role" validate:"family_role"` // Peran dalam keluarga
88
- Status *string `json:"status" validate:"life_status"` // Status (Hidup, Wafat)
89
  Religion *string `json:"religion" validate:"religion"` // Agama
90
  Job *string `json:"job"` // Pekerjaan
91
- LastEducation *string `json:"last_education" validate:"last_education"` // Pendidikan terakhir
92
  Age *int `json:"age"` // Usia
93
  }
94
 
@@ -96,11 +96,11 @@ type (
96
  ID int64 `json:"-"`
97
  AccountID int64 `json:"-"`
98
 
99
- Role *string `json:"role" validate:"family_role"` // Peran dalam keluarga
100
- Status *string `json:"status" validate:"life_status"` // Status (Hidup, Wafat)
101
  Religion *string `json:"religion" validate:"religion"` // Agama
102
  Job *string `json:"job"` // Pekerjaan
103
- LastEducation *string `json:"last_education" validate:"last_education"` // Pendidikan terakhir
104
  Age *int `json:"age"` // Usia
105
  }
106
 
@@ -120,9 +120,9 @@ type (
120
  AccountID int64 `json:"-"`
121
  HeightInCm *int `json:"height_cm"` // Tinggi badan dalam satuan sentimeter
122
  WeightInKg *int `json:"weight_kg"` // Berat badan dalam satuan kilogram
123
- BodyShape *string `json:"body_shape" validate:"body_shape"` // Bentuk tubuh
124
- SkinColor *string `json:"skin_color" validate:"skin_color"` // Warna kulit
125
- HairType *string `json:"hair_type" validate:"hair_type"` // Tipe rambut
126
  MedicalHistory *pq.StringArray `json:"medical_history"` // Riwayat penyakit
127
  PhysicalDisorder *string `json:"physical_disorder"` // Cacat fisik
128
  PhysicalTraits *string `json:"physical_traits"` // Ciri khas fisik
@@ -139,10 +139,10 @@ type (
139
  DateOfBirth *time.Time `json:"date_of_birth"`
140
  PlaceOfBirth *string `json:"place_of_birth"`
141
  Domicile *string `json:"domicile"`
142
- MaritalStatus *string `json:"marital_status" validate:"marital_status"`
143
- LastEducation *string `json:"last_education" validate:"last_education"`
144
  LastJob *string `json:"last_job"`
145
- PhoneNumber *string `json:"phone_number" validate:"phone_number"`
146
  }
147
 
148
  GetAccountDetailsRequest struct {
@@ -150,22 +150,23 @@ type (
150
  }
151
 
152
  SaveWorshipAndReligiousUnderstandingRequest struct {
153
- AccountID int64 `json:"-"`
154
- ObligatoryPrayer *string `json:"obligatory_prayer"` // sholat_wajib_5_waktu
155
- CongregationalPrayer *string `json:"congregational_prayer"` // sholat_berjamaah_di_masjid
156
- TahajjudPrayer *string `json:"tahajjud_prayer"` // sholat_tahajud
157
- DhuhaPrayer *string `json:"dhuha_prayer"` // sholat_dhuha
158
- QuranMemorization *string `json:"quran_memorization"` // hafalan_alquran
159
- QuranReadingAbility *string `json:"quran_reading_ability" validate:"quran_reading_ability"` // kemampuan_baca_alquran
160
- DaudFasting *string `json:"daud_fasting"` // puasa_daud
161
- AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
162
- HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
163
- ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
164
- OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
165
- OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
166
- OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
167
- WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
168
- FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
 
169
  }
170
 
171
  GetWorshipAndReligiousUnderstandingRequest struct {
@@ -174,7 +175,7 @@ type (
174
 
175
  CreateEducationRequest struct {
176
  AccountID int64 `json:"-"`
177
- LastEducation *string `json:"last_education" validate:"last_education"` // pendidikan terakhir
178
  EducationInstitute *string `json:"education_institute"` // institusi pendidikan
179
  EducationMajor *string `json:"education_major"` // jurusan pendidikan
180
  YearStart *int `json:"year_start"` // tahun masuk
@@ -184,7 +185,7 @@ type (
184
  UpdateEducationRequest struct {
185
  ID int64 `json:"-"`
186
  AccountID int64 `json:"-"`
187
- LastEducation *string `json:"last_education" validate:"last_education"` // pendidikan terakhir
188
  EducationInstitute *string `json:"education_institute"` // institusi pendidikan
189
  EducationMajor *string `json:"education_major"` // jurusan pendidikan
190
  YearStart *int `json:"year_start"` // tahun masuk
@@ -208,7 +209,7 @@ type (
208
  InstitutionName *string `json:"institution_name"` // nama instansi
209
  CurrentJob *string `json:"current_job"` // pekerjaan saat ini
210
  YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
211
- MonthlyIncome *string `json:"monthly_income" validate:"monthly_income"` // penghasilan per bulan
212
  IncomeSources *pq.StringArray `json:"income_sources"` // sumber penghasilan
213
  }
214
 
@@ -218,7 +219,7 @@ type (
218
  InstitutionName *string `json:"institution_name"` // nama instansi
219
  CurrentJob *string `json:"current_job"` // pekerjaan saat ini
220
  YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
221
- MonthlyIncome *string `json:"monthly_income" validate:"monthly_income"` // penghasilan per bulan
222
  IncomeSources *pq.StringArray `json:"income_sources"` // sumber penghasilan
223
  }
224
 
@@ -283,48 +284,82 @@ type (
283
  }
284
  )
285
 
286
- type SaveMarriageReadinessProfileRequest struct {
287
- AccountID int64 `json:"account_id"`
288
-
289
- // Visi Misi Rumah Tangga
290
- MarriageVision *string `gorm:"column:marriage_vision" json:"marriage_vision"` // Apa visi dan tujuan utama kamu dalam membangun rumah tangga?
291
- LivingPlan *string `gorm:"column:living_plan" json:"living_plan"` // Setelah menikah, kamu berencana tinggal di mana?
292
- SpouseRoles *string `gorm:"column:spouse_roles" json:"spouse_roles"` // Menurutmu, apa peran utama suami dan istri dalam rumah tangga?
293
- SpouseRights *string `gorm:"column:spouse_rights" json:"spouse_rights"` // Apa saja hak suami dan istri menurutmu?
294
- ConflictResolution *string `gorm:"column:conflict_resolution" json:"conflict_resolution"` // Jika terjadi konflik, bagaimana kamu menyikapinya?
295
- ParentingStyle *string `gorm:"column:parenting_style" json:"parenting_style"` // Setelah punya anak, pola pengasuhan seperti apa yang kamu inginkan?
296
-
297
- // Konsep Acara Pernikahan
298
- ReadyToMarryDuration *string `gorm:"column:ready_to_marry_duration" json:"ready_to_marry_duration"` // Setelah taaruf dimulai, berapa lama kamu butuh untuk siap menikah?
299
- WeddingConcept *string `gorm:"column:wedding_concept" json:"wedding_concept"` // Seperti apa konsep pernikahan yang kamu harapkan?
300
- WeddingFunding *string `gorm:"column:wedding_funding" json:"wedding_funding"` // Apakah kamu sudah mempersiapkan dana pernikahan? Dari mana sumbernya?
301
-
302
- // Karir Kedepannya
303
- CareerAfterMarriage *string `gorm:"column:career_after_marriage" json:"career_after_marriage"` // Apakah kamu ingin tetap bekerja setelah menikah?
304
- TimeManagement *string `gorm:"column:time_management" json:"time_management"` // Bagaimana kamu membagi waktu antara keluarga, ibadah, dan pekerjaan?
305
- CareerGoals *string `gorm:"column:career_goals" json:"career_goals"` // Apa impian karier atau cita-cita kamu dalam 5 sampai 10 tahun ke depan?
306
- SelfDevelopment *string `gorm:"column:self_development" json:"self_development"` // Apa usaha kamu untuk terus mengembangkan diri dan skill?
307
-
308
- // Pendidikan Keluarga
309
- DelayChildren *string `gorm:"column:delay_children" json:"delay_children"` // Apakah kamu berencana menunda memiliki anak?
310
- ChildEducationPlan *string `gorm:"column:child_education_plan" json:"child_education_plan"` // Apakah kamu sudah punya rencana pendidikan anak?
311
- ReligiousEmotionalBond *string `gorm:"column:religious_emotional_bond" json:"religious_emotional_bond"` // Bagaimana cara menjaga nilai agama & kedekatan emosional dengan anak?
312
- ParentingChallenges *string `gorm:"column:parenting_challenges" json:"parenting_challenges"` // Saat menghadapi tantangan pengasuhan, bagaimana kamu menyikapinya?
313
-
314
- // Finansial Keluarga
315
- MonthlyFinance *string `gorm:"column:monthly_finance" json:"monthly_finance"` // Bagaimana kamu mengelola keuangan bulanan?
316
- FamilyResponsibility *string `gorm:"column:family_responsibility" json:"family_responsibility"` // Apakah kamu masih punya tanggungan keluarga setelah menikah?
317
- DebtStatus *string `gorm:"column:debt_status" json:"debt_status"` // Apakah kamu punya cicilan/utang setelah menikah?
318
- FinanceSharing *string `gorm:"column:finance_sharing" json:"finance_sharing"` // Setelah menikah, bagaimana pembagian tanggung jawab keuangan?
319
- IncomeGapView *string `gorm:"column:income_gap_view" json:"income_gap_view"` // Pandangan kamu jika istri berpenghasilan lebih besar dari suami?
320
-
321
- // Keputusan dan Komunikasi
322
- DecisionMaking *string `gorm:"column:decision_making" json:"decision_making"` // Dalam mengambil keputusan, kamu lebih mempertimbangkan pasangan atau sendiri?
323
- GrowthTogether *string `gorm:"column:growth_together" json:"growth_together"` // Apakah kamu siap berjuang bersama pasangan? Apa saja yang ingin diperjuangkan?
324
- ParentIntervention *string `gorm:"column:parent_intervention" json:"parent_intervention"` // Sejauh mana orang tua/mertua boleh ikut campur dalam rumah tangga?
325
- HabitResponse *string `gorm:"column:habit_response" json:"habit_response"` // Bagaimana kamu menyikapi kebiasaan pasangan yang kurang kamu sukai?
326
- }
327
 
328
- type GetMarriageReadinessProfileRequest struct {
329
- AccountID int64 `json:"-"`
330
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  FavoriteFoodAndDrinks *string `json:"favorite_food_and_drinks"` // makanan dan minuman favorit
75
  CanCook *bool `json:"can_cook"` // bisa memasak
76
  TypesOfDishesCooked *string `json:"types_of_dishes_cooked"` // jenis masakan yang bisa dimasak
77
+ MonthlyExpenses *string `json:"monthly_expenses" validate:"monthly-expenses"` // pengeluaran per bulan
78
  }
79
 
80
  GetPersonalityAndPreferenceRequest struct {
 
84
  CreateFamilyMemberRequest struct {
85
  AccountID int64 `json:"-"`
86
 
87
+ Role *string `json:"role" validate:"family-role"` // Peran dalam keluarga
88
+ Status *string `json:"status" validate:"life-status"` // Status (Hidup, Wafat)
89
  Religion *string `json:"religion" validate:"religion"` // Agama
90
  Job *string `json:"job"` // Pekerjaan
91
+ LastEducation *string `json:"last_education" validate:"last-education"` // Pendidikan terakhir
92
  Age *int `json:"age"` // Usia
93
  }
94
 
 
96
  ID int64 `json:"-"`
97
  AccountID int64 `json:"-"`
98
 
99
+ Role *string `json:"role" validate:"family-role"` // Peran dalam keluarga
100
+ Status *string `json:"status" validate:"life-status"` // Status (Hidup, Wafat)
101
  Religion *string `json:"religion" validate:"religion"` // Agama
102
  Job *string `json:"job"` // Pekerjaan
103
+ LastEducation *string `json:"last_education" validate:"last-education"` // Pendidikan terakhir
104
  Age *int `json:"age"` // Usia
105
  }
106
 
 
120
  AccountID int64 `json:"-"`
121
  HeightInCm *int `json:"height_cm"` // Tinggi badan dalam satuan sentimeter
122
  WeightInKg *int `json:"weight_kg"` // Berat badan dalam satuan kilogram
123
+ BodyShape *string `json:"body_shape" validate:"body-shape"` // Bentuk tubuh
124
+ SkinColor *string `json:"skin_color" validate:"skin-color"` // Warna kulit
125
+ HairType *string `json:"hair_type" validate:"hair-type"` // Tipe rambut
126
  MedicalHistory *pq.StringArray `json:"medical_history"` // Riwayat penyakit
127
  PhysicalDisorder *string `json:"physical_disorder"` // Cacat fisik
128
  PhysicalTraits *string `json:"physical_traits"` // Ciri khas fisik
 
139
  DateOfBirth *time.Time `json:"date_of_birth"`
140
  PlaceOfBirth *string `json:"place_of_birth"`
141
  Domicile *string `json:"domicile"`
142
+ MaritalStatus *string `json:"marital_status" validate:"marital-status"`
143
+ LastEducation *string `json:"last_education" validate:"last-education"`
144
  LastJob *string `json:"last_job"`
145
+ PhoneNumber *string `json:"phone_number" validate:"phone-number"`
146
  }
147
 
148
  GetAccountDetailsRequest struct {
 
150
  }
151
 
152
  SaveWorshipAndReligiousUnderstandingRequest struct {
153
+ AccountID int64 `json:"-"`
154
+ ObligatoryPrayer *string `json:"obligatory_prayer"` // sholat_wajib_5_waktu
155
+ CongregationalPrayer *string `json:"congregational_prayer"` // sholat_berjamaah_di_masjid
156
+ TahajjudPrayer *string `json:"tahajjud_prayer"` // sholat_tahajud
157
+ DhuhaPrayer *string `json:"dhuha_prayer"` // sholat_dhuha
158
+ QuranMemorization *string `json:"quran_memorization"` // hafalan_alquran
159
+ QuranReadingAbility *string `json:"quran_reading_ability" validate:"quran-reading-ability"` // kemampuan_baca_alquran
160
+ WeeklyReligiousStudyFrequency *string `json:"weekly_religious_study_frequency" validate:"weekly-religious-study-frequency"` // kajian_yang_diikuti_dalam_sepekan
161
+ DaudFasting *string `json:"daud_fasting"` // puasa_daud
162
+ AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
163
+ HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
164
+ ListeningToMusic *string `json:"listening_to_music"` // mendengarkan_musik
165
+ OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
166
+ OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
167
+ OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
168
+ WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
169
+ FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
170
  }
171
 
172
  GetWorshipAndReligiousUnderstandingRequest struct {
 
175
 
176
  CreateEducationRequest struct {
177
  AccountID int64 `json:"-"`
178
+ LastEducation *string `json:"last_education" validate:"last-education"` // pendidikan terakhir
179
  EducationInstitute *string `json:"education_institute"` // institusi pendidikan
180
  EducationMajor *string `json:"education_major"` // jurusan pendidikan
181
  YearStart *int `json:"year_start"` // tahun masuk
 
185
  UpdateEducationRequest struct {
186
  ID int64 `json:"-"`
187
  AccountID int64 `json:"-"`
188
+ LastEducation *string `json:"last_education" validate:"last-education"` // pendidikan terakhir
189
  EducationInstitute *string `json:"education_institute"` // institusi pendidikan
190
  EducationMajor *string `json:"education_major"` // jurusan pendidikan
191
  YearStart *int `json:"year_start"` // tahun masuk
 
209
  InstitutionName *string `json:"institution_name"` // nama instansi
210
  CurrentJob *string `json:"current_job"` // pekerjaan saat ini
211
  YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
212
+ MonthlyIncome *string `json:"monthly_income" validate:"monthly-income"` // penghasilan per bulan
213
  IncomeSources *pq.StringArray `json:"income_sources"` // sumber penghasilan
214
  }
215
 
 
219
  InstitutionName *string `json:"institution_name"` // nama instansi
220
  CurrentJob *string `json:"current_job"` // pekerjaan saat ini
221
  YearStartedWorking *int `json:"year_started_working"` // tahun mulai bekerja
222
+ MonthlyIncome *string `json:"monthly_income" validate:"monthly-income"` // penghasilan per bulan
223
  IncomeSources *pq.StringArray `json:"income_sources"` // sumber penghasilan
224
  }
225
 
 
284
  }
285
  )
286
 
287
+ type (
288
+ SaveMarriageReadinessProfileRequest struct {
289
+ AccountID int64 `json:"account_id"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ // Visi Misi Rumah Tangga
292
+ MarriageVision *string `gorm:"column:marriage_vision" json:"marriage_vision"` // Apa visi dan tujuan utama kamu dalam membangun rumah tangga?
293
+ LivingPlan *string `gorm:"column:living_plan" json:"living_plan"` // Setelah menikah, kamu berencana tinggal di mana?
294
+ SpouseRoles *string `gorm:"column:spouse_roles" json:"spouse_roles"` // Menurutmu, apa peran utama suami dan istri dalam rumah tangga?
295
+ SpouseRights *string `gorm:"column:spouse_rights" json:"spouse_rights"` // Apa saja hak suami dan istri menurutmu?
296
+ ConflictResolution *string `gorm:"column:conflict_resolution" json:"conflict_resolution"` // Jika terjadi konflik, bagaimana kamu menyikapinya?
297
+ ParentingStyle *string `gorm:"column:parenting_style" json:"parenting_style"` // Setelah punya anak, pola pengasuhan seperti apa yang kamu inginkan?
298
+
299
+ // Konsep Acara Pernikahan
300
+ ReadyToMarryDuration *string `gorm:"column:ready_to_marry_duration" json:"ready_to_marry_duration"` // Setelah taaruf dimulai, berapa lama kamu butuh untuk siap menikah?
301
+ WeddingConcept *string `gorm:"column:wedding_concept" json:"wedding_concept"` // Seperti apa konsep pernikahan yang kamu harapkan?
302
+ WeddingFunding *string `gorm:"column:wedding_funding" json:"wedding_funding"` // Apakah kamu sudah mempersiapkan dana pernikahan? Dari mana sumbernya?
303
+
304
+ // Karir Kedepannya
305
+ CareerAfterMarriage *string `gorm:"column:career_after_marriage" json:"career_after_marriage"` // Apakah kamu ingin tetap bekerja setelah menikah?
306
+ TimeManagement *string `gorm:"column:time_management" json:"time_management"` // Bagaimana kamu membagi waktu antara keluarga, ibadah, dan pekerjaan?
307
+ CareerGoals *string `gorm:"column:career_goals" json:"career_goals"` // Apa impian karier atau cita-cita kamu dalam 5 sampai 10 tahun ke depan?
308
+ SelfDevelopment *string `gorm:"column:self_development" json:"self_development"` // Apa usaha kamu untuk terus mengembangkan diri dan skill?
309
+
310
+ // Pendidikan Keluarga
311
+ DelayChildren *string `gorm:"column:delay_children" json:"delay_children"` // Apakah kamu berencana menunda memiliki anak?
312
+ ChildEducationPlan *string `gorm:"column:child_education_plan" json:"child_education_plan"` // Apakah kamu sudah punya rencana pendidikan anak?
313
+ ReligiousEmotionalBond *string `gorm:"column:religious_emotional_bond" json:"religious_emotional_bond"` // Bagaimana cara menjaga nilai agama & kedekatan emosional dengan anak?
314
+ ParentingChallenges *string `gorm:"column:parenting_challenges" json:"parenting_challenges"` // Saat menghadapi tantangan pengasuhan, bagaimana kamu menyikapinya?
315
+
316
+ // Finansial Keluarga
317
+ MonthlyFinance *string `gorm:"column:monthly_finance" json:"monthly_finance"` // Bagaimana kamu mengelola keuangan bulanan?
318
+ FamilyResponsibility *string `gorm:"column:family_responsibility" json:"family_responsibility"` // Apakah kamu masih punya tanggungan keluarga setelah menikah?
319
+ DebtStatus *string `gorm:"column:debt_status" json:"debt_status"` // Apakah kamu punya cicilan/utang setelah menikah?
320
+ FinanceSharing *string `gorm:"column:finance_sharing" json:"finance_sharing"` // Setelah menikah, bagaimana pembagian tanggung jawab keuangan?
321
+ IncomeGapView *string `gorm:"column:income_gap_view" json:"income_gap_view"` // Pandangan kamu jika istri berpenghasilan lebih besar dari suami?
322
+
323
+ // Keputusan dan Komunikasi
324
+ DecisionMaking *string `gorm:"column:decision_making" json:"decision_making"` // Dalam mengambil keputusan, kamu lebih mempertimbangkan pasangan atau sendiri?
325
+ GrowthTogether *string `gorm:"column:growth_together" json:"growth_together"` // Apakah kamu siap berjuang bersama pasangan? Apa saja yang ingin diperjuangkan?
326
+ ParentIntervention *string `gorm:"column:parent_intervention" json:"parent_intervention"` // Sejauh mana orang tua/mertua boleh ikut campur dalam rumah tangga?
327
+ HabitResponse *string `gorm:"column:habit_response" json:"habit_response"` // Bagaimana kamu menyikapi kebiasaan pasangan yang kurang kamu sukai?
328
+ }
329
+
330
+ GetMarriageReadinessProfileRequest struct {
331
+ AccountID int64 `json:"-"`
332
+ }
333
+ )
334
+
335
+ type (
336
+ SavePartnerCriteriaRequest struct {
337
+ AccountID int64 `json:"account_id"`
338
+
339
+ ExpectedAgeLimit *int `gorm:"column:expected_age_limit" json:"expected_age_limit"` // batas usia pasangan yang diharapkan
340
+ ExpectedDomicile *string `gorm:"column:expected_domicile" json:"expected_domicile"` // domisili pasangan yang diharapkan
341
+ AcceptedMaritalStatus *pq.StringArray `gorm:"column:accepted_marital_status;type:varchar(255)[]" json:"accepted_marital_status"` // status pernikahan yang diterima
342
+ PartnerFamilyReligion *string `gorm:"column:partner_family_religion" json:"partner_family_religion"` // agama yang dianut keluarga pasangan
343
+ PartnerWeeklyReligiousStudyFrequency *string `gorm:"column:partner_weekly_religious_study_frequency" json:"partner_weekly_religious_study_frequency"` // rata-rata jumlah kajian yang diikuti pasangan per pekan
344
+ PartnerMonthlySpendingEstimate *string `gorm:"column:partner_monthly_spending_estimate" json:"partner_monthly_spending_estimate"` // estimasi pengeluaran pasangan per bulan
345
+ PartnerCanCook *string `gorm:"column:partner_can_cook" json:"partner_can_cook"` // apakah pasangan diharapkan bisa memasak
346
+
347
+ // Kriteria Fisik
348
+ ExpectedBodyShapes *pq.StringArray `gorm:"column:expected_body_shapes;type:varchar(255)[]" json:"expected_body_shapes"` // bentuk tubuh yang diharapkan
349
+ ExpectedSkinColors *pq.StringArray `gorm:"column:expected_skin_colors;type:varchar(255)[]" json:"expected_skin_colors"` // warna kulit yang diharapkan
350
+ ExpectedHairTypes *pq.StringArray `gorm:"column:expected_hair_types;type:varchar(255)[]" json:"expected_hair_types"` // tipe rambut yang diharapkan
351
+ ExpectedHairThickness *pq.StringArray `gorm:"column:expected_hair_thickness;type:varchar(255)[]" json:"expected_hair_thickness"` // jenis rambut yang diharapkan
352
+ ExpectedHeight *int `gorm:"column:expected_height" json:"expected_height"` // tinggi badan yang diharapkan
353
+
354
+ // Pendidikan & Pekerjaan
355
+ ExpectedPartnerIncome *string `gorm:"column:expected_partner_income" json:"expected_partner_income"` // penghasilan pasangan per bulan yang diharapkan
356
+ ExpectedIncomeSources *pq.StringArray `gorm:"column:expected_income_sources;type:varchar(255)[]" json:"expected_income_sources"` // sumber penghasilan pasangan
357
+ ExpectedLastEducation *pq.StringArray `gorm:"column:expected_last_education;type:varchar(255)[]" json:"expected_last_education"` // pendidikan terakhir yang diharapkan
358
+ ExpectedJobType *string `gorm:"column:expected_job_type" json:"expected_job_type"` // jenis pekerjaan pasangan yang diharapkan
359
+
360
+ }
361
+
362
+ GetPartnerCriteriaRequest struct {
363
+ AccountID int64 `json:"-"`
364
+ }
365
+ )
space/space/space/space/space/space/pkg/validation/custom_rules.go CHANGED
@@ -1,105 +1,154 @@
1
  package validation
2
 
3
  import (
 
 
 
4
  "regexp"
5
  "strings"
6
  "sync"
 
7
 
8
  v10 "github.com/go-playground/validator/v10"
9
  "gorm.io/gorm"
10
  )
11
 
12
- type ValidOptionSource interface {
13
  GetValidOptions(key string) ([]string, error)
14
  GetValidKeys() []string
 
 
 
15
  }
16
 
17
  // --------------------
18
- // InMemoryOptionSource
19
  // --------------------
20
 
21
- type InMemoryOptionSource struct{}
22
-
23
- var inMemoryOptions = map[string][]string{
24
- "last_education": {"SD", "SMP", "SMA", "D1", "D2", "D3", "D4", "D5", "S1", "S2", "S3"},
25
- "marital_status": {"Belum Menikah", "Duda", "Janda"},
26
- "gender": {"Laki-laki", "Perempuan"},
27
- "monthly_expenses": {"< 2 Juta", "2-5 Juta", "5-20 Juta", "> 10 Juta"},
28
- "monthly_income": {"< 3 Juta", "3-5 Juta", "5-10 Juta", "> 10 Juta"},
29
- "religion": {"Islam", "Non-Islam"},
30
- "family_role": {"Ayah", "Ibu", "Kakak", "Adik", "Anak"},
31
- "life_status": {"Hidup", "Wafat"},
32
- "body_shape": {"Ideal", "Kurus", "Berisi", "Gemuk"},
33
- "skin_color": {"Putih", "Kuning Langsat", "Sawo Matang"},
34
- "hair_type": {"Lurus", "Bergelombang", "Keriting"},
35
- "frequently": {"Selalu", "Sering", "Kadang", "Jarang", "Tidak Pernah"},
36
- "quran_reading_ability": {"Lancar", "Menengah", "Perlu Bimbingan"},
37
- }
38
-
39
- func (s *InMemoryOptionSource) GetValidOptions(key string) ([]string, error) {
40
- return inMemoryOptions[key], nil
41
  }
42
 
43
- func (s *InMemoryOptionSource) GetValidKeys() []string {
44
- keys := make([]string, 0, len(inMemoryOptions))
45
- for k := range inMemoryOptions {
46
- keys = append(keys, k)
 
47
  }
48
- return keys
 
 
 
49
  }
50
 
51
- // --------------------
52
- // DBOptionSource
53
- // --------------------
54
 
55
- type DBOptionSource struct {
56
- options map[string][]string
57
- mu sync.RWMutex
58
- }
59
 
60
- type (
61
- OptionCategory struct {
62
- ID int64 `gorm:"primaryKey" json:"id"`
63
- OptionName string `json:"option_name"`
64
- OptionSlug string `json:"option_slug" gorm:"uniqueIndex"`
65
  }
66
 
67
- OptionValues struct {
68
- ID int64 `gorm:"primaryKey" json:"id"`
69
- OptionCategoryID int64 `json:"option_category_id"`
70
- OptionValue string `json:"option_value"`
 
 
 
 
 
 
 
71
  }
72
- )
73
 
74
- func NewDBOptionSource(db *gorm.DB) (*DBOptionSource, error) {
75
- var categories []OptionCategory
76
- if err := db.Find(&categories).Error; err != nil {
77
- return nil, err
78
  }
79
 
80
- options := make(map[string][]string)
81
- for _, cat := range categories {
82
- var values []OptionValues
83
- if err := db.Where("option_category_id = ?", cat.ID).Find(&values).Error; err != nil {
84
- return nil, err
85
- }
86
- for _, val := range values {
87
- options[cat.OptionSlug] = append(options[cat.OptionSlug], val.OptionValue)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
- }
 
 
 
 
 
90
 
91
- return &DBOptionSource{options: options}, nil
 
 
 
 
92
  }
93
 
94
  func (s *DBOptionSource) GetValidOptions(key string) ([]string, error) {
 
 
 
 
 
 
 
 
 
 
95
  s.mu.RLock()
96
  defer s.mu.RUnlock()
97
- return s.options[key], nil
 
 
 
 
 
 
 
 
98
  }
99
 
100
  func (s *DBOptionSource) GetValidKeys() []string {
101
  s.mu.RLock()
102
  defer s.mu.RUnlock()
 
103
  keys := make([]string, 0, len(s.options))
104
  for k := range s.options {
105
  keys = append(keys, k)
@@ -108,58 +157,144 @@ func (s *DBOptionSource) GetValidKeys() []string {
108
  }
109
 
110
  // --------------------
111
- // Validator
112
  // --------------------
113
 
114
  type Validator struct {
115
- source ValidOptionSource
 
116
  }
117
 
118
- func NewValidatorRules(source ValidOptionSource) *Validator {
119
- return &Validator{source: source}
 
 
 
 
120
  }
121
 
122
- func (v *Validator) GenericOptionRule(key string) func(fl v10.FieldLevel) bool {
123
- return func(fl v10.FieldLevel) bool {
124
- value := fl.Field().String()
125
- if value == "" {
126
- return true
 
 
 
 
 
127
  }
128
- validOptions, err := v.source.GetValidOptions(key)
129
- if err != nil {
130
- return false
 
 
 
131
  }
132
- for _, opt := range validOptions {
133
- if opt == value {
134
- return true
135
- }
136
  }
137
- return false
138
  }
 
 
139
  }
 
 
 
 
140
 
141
- func (v *Validator) RegisterAllCustomRules(validate *v10.Validate) error {
142
- for _, key := range v.source.GetValidKeys() {
143
- err := validate.RegisterValidation(key, v.GenericOptionRule(key))
144
- if err != nil {
145
- return err
146
  }
 
147
  }
148
 
149
- err := validate.RegisterValidation("password", v.PasswordRule)
150
- if err != nil {
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
 
162
- func (v *Validator) PasswordRule(fl v10.FieldLevel) bool {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  password := fl.Field().String()
164
  return len(password) >= 8 &&
165
  strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") &&
@@ -167,38 +302,23 @@ func (v *Validator) PasswordRule(fl v10.FieldLevel) bool {
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
  }
 
1
  package validation
2
 
3
  import (
4
+ "context"
5
+ "fmt"
6
+ "reflect"
7
  "regexp"
8
  "strings"
9
  "sync"
10
+ "time"
11
 
12
  v10 "github.com/go-playground/validator/v10"
13
  "gorm.io/gorm"
14
  )
15
 
16
+ type ValidatorOptionSource interface {
17
  GetValidOptions(key string) ([]string, error)
18
  GetValidKeys() []string
19
+ Refresh() error
20
+ StartAutoRefresh(ctx context.Context, interval time.Duration)
21
+ HasKey(key string) bool
22
  }
23
 
24
  // --------------------
25
+ // DBOptionSource with Safe Handling
26
  // --------------------
27
 
28
+ type DBOptionSource struct {
29
+ db *gorm.DB
30
+ options map[string][]string
31
+ mu sync.RWMutex
32
+ lastUpdate time.Time
33
+ expiry time.Duration
34
+ stopChan chan struct{}
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
+ func NewDBOptionSource(db *gorm.DB, expiry time.Duration) (*DBOptionSource, error) {
38
+ source := &DBOptionSource{
39
+ db: db,
40
+ expiry: expiry,
41
+ stopChan: make(chan struct{}),
42
  }
43
+ if err := source.Refresh(); err != nil {
44
+ return nil, fmt.Errorf("failed to initialize DB option source: %w", err)
45
+ }
46
+ return source, nil
47
  }
48
 
49
+ func (s *DBOptionSource) Refresh() error {
50
+ s.mu.Lock()
51
+ defer s.mu.Unlock()
52
 
53
+ // Buat session baru tanpa transaction
54
+ tx := s.db.Session(&gorm.Session{SkipDefaultTransaction: true})
 
 
55
 
56
+ var results []struct {
57
+ Slug string `gorm:"column:slug"`
58
+ Value string `gorm:"column:value"`
 
 
59
  }
60
 
61
+ err := tx.Raw(`
62
+ SELECT
63
+ c.option_slug AS slug,
64
+ v.option_value AS value
65
+ FROM option_categories c
66
+ JOIN option_values v ON c.id = v.option_category_id
67
+ ORDER BY c.id, v.id
68
+ `).Scan(&results).Error
69
+
70
+ if err != nil {
71
+ return fmt.Errorf("failed to refresh options: %w", err)
72
  }
 
73
 
74
+ newOptions := make(map[string][]string)
75
+ for _, r := range results {
76
+ newOptions[r.Slug] = append(newOptions[r.Slug], r.Value)
 
77
  }
78
 
79
+ s.options = newOptions
80
+ s.lastUpdate = time.Now()
81
+
82
+ fmt.Println("options refreshed")
83
+
84
+ return nil
85
+ }
86
+
87
+ func (s *DBOptionSource) StartAutoRefresh(ctx context.Context, interval time.Duration) {
88
+ ticker := time.NewTicker(interval)
89
+ go func() {
90
+ for {
91
+ select {
92
+ case <-ticker.C:
93
+ s.mu.Lock()
94
+ needsRefresh := time.Since(s.lastUpdate) > s.expiry
95
+ s.mu.Unlock()
96
+
97
+ if needsRefresh {
98
+ if err := s.Refresh(); err != nil {
99
+ fmt.Printf("failed to auto-refresh options: %v\n", err)
100
+ }
101
+ }
102
+ case <-ctx.Done():
103
+ ticker.Stop()
104
+ return
105
+ case <-s.stopChan:
106
+ ticker.Stop()
107
+ return
108
+ }
109
  }
110
+ }()
111
+ }
112
+
113
+ func (s *DBOptionSource) StopAutoRefresh() {
114
+ close(s.stopChan)
115
+ }
116
 
117
+ func (s *DBOptionSource) HasKey(key string) bool {
118
+ s.mu.RLock()
119
+ defer s.mu.RUnlock()
120
+ _, exists := s.options[key]
121
+ return exists
122
  }
123
 
124
  func (s *DBOptionSource) GetValidOptions(key string) ([]string, error) {
125
+ s.mu.RLock()
126
+ needsRefresh := time.Since(s.lastUpdate) > s.expiry
127
+ s.mu.RUnlock()
128
+
129
+ if needsRefresh {
130
+ if err := s.Refresh(); err != nil {
131
+ return nil, fmt.Errorf("failed to refresh options: %w", err)
132
+ }
133
+ }
134
+
135
  s.mu.RLock()
136
  defer s.mu.RUnlock()
137
+
138
+ options, exists := s.options[key]
139
+ if !exists {
140
+ return nil, nil // Return nil instead of error for missing keys
141
+ }
142
+
143
+ copied := make([]string, len(options))
144
+ copy(copied, options)
145
+ return copied, nil
146
  }
147
 
148
  func (s *DBOptionSource) GetValidKeys() []string {
149
  s.mu.RLock()
150
  defer s.mu.RUnlock()
151
+
152
  keys := make([]string, 0, len(s.options))
153
  for k := range s.options {
154
  keys = append(keys, k)
 
157
  }
158
 
159
  // --------------------
160
+ // Validator with Safe Rule Handling
161
  // --------------------
162
 
163
  type Validator struct {
164
+ source ValidatorOptionSource
165
+ validate *v10.Validate
166
  }
167
 
168
+ func NewValidator(source ValidatorOptionSource) *Validator {
169
+ validate := v10.New()
170
+ return &Validator{
171
+ source: source,
172
+ validate: validate,
173
+ }
174
  }
175
 
176
+ func (v *Validator) RegisterAllCustomRules() error {
177
+ // First register static validation rules
178
+ staticRules := map[string]func(v10.FieldLevel) bool{
179
+ "password": v.validatePassword,
180
+ "phone-number": v.validatePhoneNumber,
181
+ }
182
+
183
+ for name, fn := range staticRules {
184
+ if err := v.validate.RegisterValidation(name, fn); err != nil {
185
+ return fmt.Errorf("failed to register %s validation: %w", name, err)
186
  }
187
+ }
188
+
189
+ // Then register dynamic option rules
190
+ for _, key := range v.source.GetValidKeys() {
191
+ if !v.source.HasKey(key) {
192
+ continue // Skip if key doesn't exist
193
  }
194
+
195
+ if err := v.validate.RegisterValidation(key, v.createOptionRule(key)); err != nil {
196
+ return fmt.Errorf("failed to register validation for %s: %w", key, err)
 
197
  }
 
198
  }
199
+
200
+ return nil
201
  }
202
+ func (v *Validator) Validate(input interface{}) error {
203
+ if input == nil {
204
+ return nil
205
+ }
206
 
207
+ val := reflect.ValueOf(input)
208
+ if val.Kind() == reflect.Ptr {
209
+ if val.IsNil() {
210
+ return nil
 
211
  }
212
+ val = val.Elem() // Dereference the pointer
213
  }
214
 
215
+ err := v.validate.Struct(input)
216
+ if err == nil {
217
+ return nil
218
  }
219
 
220
+ // Filter out errors for nil/empty fields
221
+ if ve, ok := err.(v10.ValidationErrors); ok {
222
+ var filteredErrors v10.ValidationErrors
223
+ for _, fe := range ve {
224
+ fieldValue := val.FieldByName(fe.StructField())
225
+ if !fieldValue.IsValid() {
226
+ continue
227
+ }
228
+
229
+ if !isEmpty(fieldValue) {
230
+ filteredErrors = append(filteredErrors, fe)
231
+ } else {
232
+ fmt.Printf("Ignoring validation error for empty field: %s\n", fe.Field())
233
+ }
234
+ }
235
+
236
+ if len(filteredErrors) > 0 {
237
+ return filteredErrors
238
+ }
239
+ return nil
240
  }
241
 
242
+ return err
243
+ }
244
+
245
+ // isEmpty checks if a value is nil or empty
246
+ func isEmpty(v reflect.Value) bool {
247
+ switch v.Kind() {
248
+ case reflect.String:
249
+ return v.Len() == 0
250
+ case reflect.Ptr, reflect.Interface:
251
+ return v.IsNil()
252
+ case reflect.Slice, reflect.Map, reflect.Array:
253
+ return v.Len() == 0
254
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
255
+ return v.Int() == 0
256
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
257
+ return v.Uint() == 0
258
+ case reflect.Float32, reflect.Float64:
259
+ return v.Float() == 0
260
+ case reflect.Bool:
261
+ return !v.Bool()
262
+ case reflect.Struct:
263
+ if t, ok := v.Interface().(time.Time); ok {
264
+ return t.IsZero()
265
+ }
266
+ // Consider non-time structs as non-empty
267
+ return false
268
+ default:
269
+ return false
270
+ }
271
  }
272
 
273
+ // createOptionRule remains the same as previous version
274
+ func (v *Validator) createOptionRule(key string) func(v10.FieldLevel) bool {
275
+ return func(fl v10.FieldLevel) bool {
276
+ field := fl.Field()
277
+ if isEmpty(field) {
278
+ return true
279
+ }
280
+
281
+ value := field.String()
282
+ validOptions, err := v.source.GetValidOptions(key)
283
+ if err != nil || validOptions == nil {
284
+ return true
285
+ }
286
+
287
+ for _, opt := range validOptions {
288
+ if opt == value {
289
+ return true
290
+ }
291
+ }
292
+
293
+ return false
294
+ }
295
+ }
296
+
297
+ func (v *Validator) validatePassword(fl v10.FieldLevel) bool {
298
  password := fl.Field().String()
299
  return len(password) >= 8 &&
300
  strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") &&
 
302
  strings.ContainsAny(password, "0123456789")
303
  }
304
 
305
+ func (v *Validator) validatePhoneNumber(fl v10.FieldLevel) bool {
306
+ phone := NormalizePhoneNumber(fl.Field().String())
307
  return strings.HasPrefix(phone, "+62")
308
  }
309
 
310
+ func NormalizePhoneNumber(input string) string {
 
 
 
 
 
 
 
311
  re := regexp.MustCompile(`[^0-9\+]`)
312
  input = re.ReplaceAllString(input, "")
313
 
314
+ switch {
315
+ case strings.HasPrefix(input, "0"):
316
+ return "+62" + input[1:]
317
+ case strings.HasPrefix(input, "62") && !strings.HasPrefix(input, "+62"):
318
+ return "+" + input
319
+ case strings.HasPrefix(input, "8"):
320
+ return "+62" + input
321
+ default:
322
+ return input
323
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  }
space/space/space/space/space/space/repositories/partner_criteria_repository.go ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package repositories
2
+
3
+ import (
4
+ "context"
5
+
6
+ "api.qobiltu.id/models"
7
+ "gorm.io/gorm"
8
+ )
9
+
10
+ type PartnerCriteriaRepository interface {
11
+ SavePartnerCriteria(ctx context.Context, req *models.PartnerCriteria) (*models.PartnerCriteria, error)
12
+ GetPartnerCriteria(ctx context.Context, accountID int64) (*models.PartnerCriteria, error)
13
+ }
14
+
15
+ type partnerCriteriaRepository struct {
16
+ db *gorm.DB
17
+ }
18
+
19
+ func NewPartnerCriteriaRepository(db *gorm.DB) PartnerCriteriaRepository {
20
+ return &partnerCriteriaRepository{
21
+ db: db,
22
+ }
23
+ }
24
+
25
+ func (r *partnerCriteriaRepository) SavePartnerCriteria(ctx context.Context, req *models.PartnerCriteria) (*models.PartnerCriteria, error) {
26
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
27
+ return req, err
28
+ }
29
+ return req, nil
30
+ }
31
+
32
+ func (r *partnerCriteriaRepository) GetPartnerCriteria(ctx context.Context, accountID int64) (*models.PartnerCriteria, error) {
33
+ var partnerCriteria models.PartnerCriteria
34
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&partnerCriteria).Error; err != nil {
35
+ return nil, err
36
+ }
37
+ return &partnerCriteria, nil
38
+ }
space/space/space/space/space/space/router/partner_criteria_route.go ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package router
2
+
3
+ import "api.qobiltu.id/middleware"
4
+
5
+ func (s *Server) PartnerCriteriaRoute() {
6
+ routerGroup := s.router.Group("/api/v1/partner-criteria").Use(middleware.AuthUser)
7
+ {
8
+ routerGroup.POST("", s.partnerCriteriaController.SavePartnerCriteria)
9
+ routerGroup.GET("", s.partnerCriteriaController.GetPartnerCriteria)
10
+ }
11
+ }
space/space/space/space/space/space/router/router.go CHANGED
@@ -19,5 +19,6 @@ func (s *Server) setupRoutes() {
19
  s.HealthCheckRoute()
20
  s.CVRoute()
21
  s.MarriageReadinessProfileRoute()
 
22
  s.StorageRoute()
23
  }
 
19
  s.HealthCheckRoute()
20
  s.CVRoute()
21
  s.MarriageReadinessProfileRoute()
22
+ s.PartnerCriteriaRoute()
23
  s.StorageRoute()
24
  }
space/space/space/space/space/space/router/server.go CHANGED
@@ -4,6 +4,7 @@ import (
4
  cv_controller "api.qobiltu.id/controller/cv"
5
  health_check_controller "api.qobiltu.id/controller/health_check"
6
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
 
7
  "github.com/gin-gonic/gin"
8
  )
9
 
@@ -12,12 +13,14 @@ type Server struct {
12
  healthCheckController health_check_controller.HealthCheckController
13
  cvController cv_controller.CVController
14
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
 
15
  }
16
 
17
  func NewServer(
18
  healthCheckController health_check_controller.HealthCheckController,
19
  cvController cv_controller.CVController,
20
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
 
21
  ) (*Server, error) {
22
 
23
  router := gin.Default()
@@ -27,6 +30,7 @@ func NewServer(
27
  healthCheckController: healthCheckController,
28
  cvController: cvController,
29
  marriageReadinessProfileController: marriageReadinessProfileController,
 
30
  router: router,
31
  }
32
 
 
4
  cv_controller "api.qobiltu.id/controller/cv"
5
  health_check_controller "api.qobiltu.id/controller/health_check"
6
  marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
7
+ partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
8
  "github.com/gin-gonic/gin"
9
  )
10
 
 
13
  healthCheckController health_check_controller.HealthCheckController
14
  cvController cv_controller.CVController
15
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
16
+ partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
17
  }
18
 
19
  func NewServer(
20
  healthCheckController health_check_controller.HealthCheckController,
21
  cvController cv_controller.CVController,
22
  marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
23
+ partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
24
  ) (*Server, error) {
25
 
26
  router := gin.Default()
 
30
  healthCheckController: healthCheckController,
31
  cvController: cvController,
32
  marriageReadinessProfileController: marriageReadinessProfileController,
33
+ partnerCriteriaController: partnerCriteriaController,
34
  router: router,
35
  }
36
 
space/space/space/space/space/space/services/partner_criteria_service.go ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package services
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+
7
+ "api.qobiltu.id/models"
8
+ "api.qobiltu.id/pkg/validation"
9
+ "api.qobiltu.id/repositories"
10
+ "api.qobiltu.id/response"
11
+ "api.qobiltu.id/utils"
12
+ "gorm.io/gorm"
13
+ )
14
+
15
+ type PartnerCriteriaService interface {
16
+ SavePartnerCriteria(ctx context.Context, req *models.SavePartnerCriteriaRequest) (*models.PartnerCriteria, error)
17
+ GetPartnerCriteria(ctx context.Context, req *models.GetPartnerCriteriaRequest) (*models.PartnerCriteria, error)
18
+ }
19
+
20
+ type partnerCriteriaService struct {
21
+ partnerCriteriaRepository repositories.PartnerCriteriaRepository
22
+ validator *validation.Validator
23
+ }
24
+
25
+ func NewPartnerCriteriaService(partnerCriteriaRepository repositories.PartnerCriteriaRepository, validator *validation.Validator) PartnerCriteriaService {
26
+ return &partnerCriteriaService{partnerCriteriaRepository: partnerCriteriaRepository, validator: validator}
27
+ }
28
+
29
+ func (s *partnerCriteriaService) SavePartnerCriteria(ctx context.Context, req *models.SavePartnerCriteriaRequest) (*models.PartnerCriteria, error) {
30
+ if err := s.validator.Validate(req); err != nil {
31
+ return nil, response.HandleValidationError(err)
32
+ }
33
+
34
+ partnerCriteria, err := s.partnerCriteriaRepository.GetPartnerCriteria(ctx, req.AccountID)
35
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
36
+ return nil, response.HandleGormError(err, "Internal Server Error")
37
+ }
38
+
39
+ if partnerCriteria == nil {
40
+ partnerCriteria = &models.PartnerCriteria{}
41
+ }
42
+
43
+ partnerCriteria.AccountID = req.AccountID
44
+
45
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedAgeLimit, req.ExpectedAgeLimit)
46
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedDomicile, req.ExpectedDomicile)
47
+ utils.AssignIfNotNil(&partnerCriteria.AcceptedMaritalStatus, req.AcceptedMaritalStatus)
48
+ utils.AssignIfNotNil(&partnerCriteria.PartnerFamilyReligion, req.PartnerFamilyReligion)
49
+ utils.AssignIfNotNil(&partnerCriteria.PartnerWeeklyReligiousStudyFrequency, req.PartnerWeeklyReligiousStudyFrequency)
50
+ utils.AssignIfNotNil(&partnerCriteria.PartnerMonthlySpendingEstimate, req.PartnerMonthlySpendingEstimate)
51
+ utils.AssignIfNotNil(&partnerCriteria.PartnerCanCook, req.PartnerCanCook)
52
+
53
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedBodyShapes, req.ExpectedBodyShapes)
54
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedSkinColors, req.ExpectedSkinColors)
55
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedHairTypes, req.ExpectedHairTypes)
56
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedHairThickness, req.ExpectedHairThickness)
57
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedHeight, req.ExpectedHeight)
58
+
59
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedPartnerIncome, req.ExpectedPartnerIncome)
60
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedIncomeSources, req.ExpectedIncomeSources)
61
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedLastEducation, req.ExpectedLastEducation)
62
+ utils.AssignIfNotNil(&partnerCriteria.ExpectedJobType, req.ExpectedJobType)
63
+
64
+ res, err := s.partnerCriteriaRepository.SavePartnerCriteria(ctx, partnerCriteria)
65
+ if err != nil {
66
+ return nil, response.HandleGormError(err, "Internal Server Error")
67
+ }
68
+
69
+ return res, nil
70
+ }
71
+
72
+ func (s *partnerCriteriaService) GetPartnerCriteria(ctx context.Context, req *models.GetPartnerCriteriaRequest) (*models.PartnerCriteria, error) {
73
+ res, err := s.partnerCriteriaRepository.GetPartnerCriteria(ctx, req.AccountID)
74
+ if err != nil {
75
+ return nil, response.HandleGormError(err, "Internal Server Error")
76
+ }
77
+ return res, nil
78
+ }
space/space/space/space/space/space/space/config/database_connection_config.go CHANGED
@@ -3,7 +3,6 @@ package config
3
  import (
4
  "fmt"
5
  "log"
6
- "log/slog"
7
  "os"
8
 
9
  "gorm.io/driver/postgres"
@@ -78,11 +77,23 @@ func AutoMigrateAll(db *gorm.DB) {
78
  &models.JobCV{},
79
  &models.AchievementCV{},
80
  &models.MarriageReadinessProfile{},
 
81
  )
82
 
83
  if err != nil {
84
  log.Fatal(err)
85
  }
86
 
87
- slog.Info("Auto-migration completed successfully")
 
 
 
 
 
 
 
 
 
 
 
88
  }
 
3
  import (
4
  "fmt"
5
  "log"
 
6
  "os"
7
 
8
  "gorm.io/driver/postgres"
 
77
  &models.JobCV{},
78
  &models.AchievementCV{},
79
  &models.MarriageReadinessProfile{},
80
+ &models.PartnerCriteria{},
81
  )
82
 
83
  if err != nil {
84
  log.Fatal(err)
85
  }
86
 
87
+ sequences := []string{
88
+ `CREATE SEQUENCE IF NOT EXISTS seq_ikh_counter START 1 INCREMENT 1 MINVALUE 1;`,
89
+ `CREATE SEQUENCE IF NOT EXISTS seq_akh_counter START 1 INCREMENT 1 MINVALUE 1;`,
90
+ }
91
+
92
+ for _, seq := range sequences {
93
+ if err := db.Exec(seq).Error; err != nil {
94
+ fmt.Printf("Gagal membuat sequence: %v", err)
95
+ }
96
+ }
97
+
98
+ fmt.Println("Auto-migration sequence check completed successfully")
99
  }
space/space/space/space/space/space/space/models/sequence.go ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ )
7
+
8
+ const (
9
+ SeqIkhCounter = "seq_ikh_counter"
10
+ SeqAkhCounter = "seq_akh_counter"
11
+ )
12
+
13
+ func GetSequenceName(gender string) string {
14
+ if strings.ToLower(gender) == "laki-laki" {
15
+ return SeqIkhCounter
16
+ }
17
+ return SeqAkhCounter
18
+ }
19
+
20
+ func BuildInitialName(gender string, number int64) string {
21
+ if strings.ToLower(gender) == "laki-laki" {
22
+ return fmt.Sprintf("IKH_%d", number)
23
+ }
24
+ return fmt.Sprintf("AKH_%d", number)
25
+ }
space/space/space/space/space/space/space/repositories/account_repository.go CHANGED
@@ -85,3 +85,21 @@ func UpdateAccountDetails(accountDetails models.AccountDetails) Repository[model
85
  repo.Result = accountDetails
86
  return *repo
87
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  repo.Result = accountDetails
86
  return *repo
87
  }
88
+
89
+ func GetNextInitialNameNumber(gender string) Repository[string, int] {
90
+ repo := Construct[string, int](gender)
91
+ sequenceName := models.GetSequenceName(gender)
92
+ repo.Transaction.Raw("SELECT nextval('" + sequenceName + "')").Scan(&repo.Result)
93
+ return *repo
94
+ }
95
+
96
+ func GetAccountDetailsByAccountID(accountID uint) Repository[models.AccountDetails, models.AccountDetails] {
97
+ repo := Construct[models.AccountDetails, models.AccountDetails](
98
+ models.AccountDetails{AccountID: accountID},
99
+ )
100
+ repo.Transactions(
101
+ WhereGivenConstructor[models.AccountDetails, models.AccountDetails],
102
+ Find[models.AccountDetails, models.AccountDetails],
103
+ )
104
+ return *repo
105
+ }
space/space/space/space/space/space/space/repositories/cv_repository.go CHANGED
@@ -1,243 +1,259 @@
1
  package repositories
2
 
3
  import (
4
- "api.qobiltu.id/models"
5
- "context"
6
- "gorm.io/gorm"
 
 
7
  )
8
 
9
  type CVRepository interface {
10
- SaveAccountDetails(ctx context.Context, req *models.AccountDetails) (*models.AccountDetails, error)
11
- GetAccountDetailsByAccountID(ctx context.Context, accountID int64) (*models.AccountDetails, error)
12
-
13
- SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCV) (*models.PersonalityAndPreferenceCV, error)
14
- GetPersonalityAndPreferenceByAccountID(ctx context.Context, accountID int64) (*models.PersonalityAndPreferenceCV, error)
15
-
16
- SaveFamilyMember(ctx context.Context, req *models.FamilyMemberCV) (*models.FamilyMemberCV, error)
17
- ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
18
- GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
19
- DeleteFamilyMember(ctx context.Context, id int64) error
20
-
21
- SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthCV) (*models.PhysicalAndHealthCV, error)
22
- GetPhysicalAndHealthByAccountID(ctx context.Context, accountID int64) (*models.PhysicalAndHealthCV, error)
23
-
24
- SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingCV) (*models.WorshipAndReligiousUnderstandingCV, error)
25
- GetWorshipAndReligiousUnderstandingByAccountID(ctx context.Context, accountID int64) (*models.WorshipAndReligiousUnderstandingCV, error)
26
-
27
- SaveEducation(ctx context.Context, req *models.EducationCV) (*models.EducationCV, error)
28
- ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
29
- GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
30
- DeleteEducation(ctx context.Context, id int64) error
31
-
32
- SaveJob(ctx context.Context, req *models.JobCV) (*models.JobCV, error)
33
- ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
34
- GetJob(ctx context.Context, id int64) (*models.JobCV, error)
35
- DeleteJob(ctx context.Context, id int64) error
36
-
37
- SaveAchievement(ctx context.Context, req *models.AchievementCV) (*models.AchievementCV, error)
38
- ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
39
- GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
40
- DeleteAchievement(ctx context.Context, id int64) error
 
41
  }
42
 
43
  type cvRepository struct {
44
- db *gorm.DB
45
  }
46
 
47
  func NewCVRepository(db *gorm.DB) CVRepository {
48
- return &cvRepository{
49
- db: db,
50
- }
51
  }
52
 
53
  func (r *cvRepository) SaveAccountDetails(ctx context.Context, req *models.AccountDetails) (*models.AccountDetails, error) {
54
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
55
- return req, err
56
- }
57
- return req, nil
58
  }
59
 
60
  func (r *cvRepository) GetAccountDetailsByAccountID(ctx context.Context, accountID int64) (*models.AccountDetails, error) {
61
- var accountDetails models.AccountDetails
62
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&accountDetails).Error; err != nil {
63
- return nil, err
64
- }
65
- return &accountDetails, nil
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
  func (r *cvRepository) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCV) (*models.PersonalityAndPreferenceCV, error) {
69
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
70
- return req, err
71
- }
72
- return req, nil
73
  }
74
 
75
  func (r *cvRepository) GetPersonalityAndPreferenceByAccountID(ctx context.Context, accountID int64) (*models.PersonalityAndPreferenceCV, error) {
76
- var personalityAndPreference models.PersonalityAndPreferenceCV
77
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&personalityAndPreference).Error; err != nil {
78
- return nil, err
79
- }
80
- return &personalityAndPreference, nil
81
  }
82
 
83
  func (r *cvRepository) SaveFamilyMember(ctx context.Context, req *models.FamilyMemberCV) (*models.FamilyMemberCV, error) {
84
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
85
- return req, err
86
- }
87
- return req, nil
88
  }
89
 
90
  func (r *cvRepository) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
91
- var familyMember models.FamilyMemberCV
92
- if err := r.db.WithContext(ctx).Where("id = ?", id).First(&familyMember).Error; err != nil {
93
- return nil, err
94
- }
95
- return &familyMember, nil
96
  }
97
 
98
  func (r *cvRepository) DeleteFamilyMember(ctx context.Context, id int64) error {
99
- if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.FamilyMemberCV{}).Error; err != nil {
100
- return err
101
- }
102
- return nil
103
  }
104
 
105
  func (r *cvRepository) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
106
- familyMembers := make([]models.FamilyMemberCV, 0)
107
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&familyMembers).Error; err != nil {
108
- return nil, err
109
- }
110
- return familyMembers, nil
111
  }
112
 
113
  func (r *cvRepository) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthCV) (*models.PhysicalAndHealthCV, error) {
114
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
115
- return req, err
116
- }
117
- return req, nil
118
  }
119
 
120
  func (r *cvRepository) GetPhysicalAndHealthByAccountID(ctx context.Context, accountID int64) (*models.PhysicalAndHealthCV, error) {
121
- var physicalAndHealth models.PhysicalAndHealthCV
122
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&physicalAndHealth).Error; err != nil {
123
- return nil, err
124
- }
125
- return &physicalAndHealth, nil
126
  }
127
 
128
  func (r *cvRepository) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingCV) (*models.WorshipAndReligiousUnderstandingCV, error) {
129
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
130
- return req, err
131
- }
132
- return req, nil
133
  }
134
 
135
  func (r *cvRepository) GetWorshipAndReligiousUnderstandingByAccountID(ctx context.Context, accountID int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
136
- var worshipAndReligiousUnderstanding models.WorshipAndReligiousUnderstandingCV
137
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&worshipAndReligiousUnderstanding).Error; err != nil {
138
- return nil, err
139
- }
140
- return &worshipAndReligiousUnderstanding, nil
141
  }
142
 
143
  // SaveEducation menyimpan atau memperbarui data pendidikan ke database
144
  func (r *cvRepository) SaveEducation(ctx context.Context, req *models.EducationCV) (*models.EducationCV, error) {
145
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
146
- return req, err
147
- }
148
- return req, nil
149
  }
150
 
151
  // ListEducation mengambil daftar data pendidikan berdasarkan account_id
152
  func (r *cvRepository) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
153
- var educations []models.EducationCV
154
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&educations).Error; err != nil {
155
- return nil, err
156
- }
157
- return educations, nil
158
  }
159
 
160
  // GetEducation mengambil satu data pendidikan berdasarkan id
161
  func (r *cvRepository) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
162
- var education models.EducationCV
163
- if err := r.db.WithContext(ctx).Where("id = ?", id).First(&education).Error; err != nil {
164
- return nil, err
165
- }
166
- return &education, nil
167
  }
168
 
169
  // DeleteEducation menghapus data pendidikan berdasarkan id
170
  func (r *cvRepository) DeleteEducation(ctx context.Context, id int64) error {
171
- if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.EducationCV{}).Error; err != nil {
172
- return err
173
- }
174
- return nil
175
  }
176
 
177
  // SaveJob menyimpan atau memperbarui data pekerjaan ke database
178
  func (r *cvRepository) SaveJob(ctx context.Context, req *models.JobCV) (*models.JobCV, error) {
179
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
180
- return req, err
181
- }
182
- return req, nil
183
  }
184
 
185
  // ListJob mengambil daftar data pekerjaan berdasarkan account_id
186
  func (r *cvRepository) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
187
- var jobs []models.JobCV
188
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&jobs).Error; err != nil {
189
- return nil, err
190
- }
191
- return jobs, nil
192
  }
193
 
194
  // GetJob mengambil satu data pekerjaan berdasarkan id
195
  func (r *cvRepository) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
196
- var job models.JobCV
197
- if err := r.db.WithContext(ctx).Where("id = ?", id).First(&job).Error; err != nil {
198
- return nil, err
199
- }
200
- return &job, nil
201
  }
202
 
203
  // DeleteJob menghapus data pekerjaan berdasarkan id
204
  func (r *cvRepository) DeleteJob(ctx context.Context, id int64) error {
205
- if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.JobCV{}).Error; err != nil {
206
- return err
207
- }
208
- return nil
209
  }
210
 
211
  // SaveAchievement menyimpan atau memperbarui data prestasi ke database
212
  func (r *cvRepository) SaveAchievement(ctx context.Context, req *models.AchievementCV) (*models.AchievementCV, error) {
213
- if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
214
- return req, err
215
- }
216
- return req, nil
217
  }
218
 
219
  // ListAchievement mengambil daftar data prestasi berdasarkan account_id
220
  func (r *cvRepository) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
221
- var achievements []models.AchievementCV
222
- if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&achievements).Error; err != nil {
223
- return nil, err
224
- }
225
- return achievements, nil
226
  }
227
 
228
  // GetAchievement mengambil satu data prestasi berdasarkan id
229
  func (r *cvRepository) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
230
- var achievement models.AchievementCV
231
- if err := r.db.WithContext(ctx).Where("id = ?", id).First(&achievement).Error; err != nil {
232
- return nil, err
233
- }
234
- return &achievement, nil
235
  }
236
 
237
  // DeleteAchievement menghapus data prestasi berdasarkan id
238
  func (r *cvRepository) DeleteAchievement(ctx context.Context, id int64) error {
239
- if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.AchievementCV{}).Error; err != nil {
240
- return err
241
- }
242
- return nil
243
  }
 
1
  package repositories
2
 
3
  import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "api.qobiltu.id/models"
8
+ "gorm.io/gorm"
9
  )
10
 
11
  type CVRepository interface {
12
+ SaveAccountDetails(ctx context.Context, req *models.AccountDetails) (*models.AccountDetails, error)
13
+ GetAccountDetailsByAccountID(ctx context.Context, accountID int64) (*models.AccountDetails, error)
14
+ GetNextInitialNameNumber(ctx context.Context, gender string) (int64, error)
15
+
16
+ SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCV) (*models.PersonalityAndPreferenceCV, error)
17
+ GetPersonalityAndPreferenceByAccountID(ctx context.Context, accountID int64) (*models.PersonalityAndPreferenceCV, error)
18
+
19
+ SaveFamilyMember(ctx context.Context, req *models.FamilyMemberCV) (*models.FamilyMemberCV, error)
20
+ ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error)
21
+ GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error)
22
+ DeleteFamilyMember(ctx context.Context, id int64) error
23
+
24
+ SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthCV) (*models.PhysicalAndHealthCV, error)
25
+ GetPhysicalAndHealthByAccountID(ctx context.Context, accountID int64) (*models.PhysicalAndHealthCV, error)
26
+
27
+ SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingCV) (*models.WorshipAndReligiousUnderstandingCV, error)
28
+ GetWorshipAndReligiousUnderstandingByAccountID(ctx context.Context, accountID int64) (*models.WorshipAndReligiousUnderstandingCV, error)
29
+
30
+ SaveEducation(ctx context.Context, req *models.EducationCV) (*models.EducationCV, error)
31
+ ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error)
32
+ GetEducation(ctx context.Context, id int64) (*models.EducationCV, error)
33
+ DeleteEducation(ctx context.Context, id int64) error
34
+
35
+ SaveJob(ctx context.Context, req *models.JobCV) (*models.JobCV, error)
36
+ ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error)
37
+ GetJob(ctx context.Context, id int64) (*models.JobCV, error)
38
+ DeleteJob(ctx context.Context, id int64) error
39
+
40
+ SaveAchievement(ctx context.Context, req *models.AchievementCV) (*models.AchievementCV, error)
41
+ ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error)
42
+ GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error)
43
+ DeleteAchievement(ctx context.Context, id int64) error
44
  }
45
 
46
  type cvRepository struct {
47
+ db *gorm.DB
48
  }
49
 
50
  func NewCVRepository(db *gorm.DB) CVRepository {
51
+ return &cvRepository{
52
+ db: db,
53
+ }
54
  }
55
 
56
  func (r *cvRepository) SaveAccountDetails(ctx context.Context, req *models.AccountDetails) (*models.AccountDetails, error) {
57
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
58
+ return req, err
59
+ }
60
+ return req, nil
61
  }
62
 
63
  func (r *cvRepository) GetAccountDetailsByAccountID(ctx context.Context, accountID int64) (*models.AccountDetails, error) {
64
+ var accountDetails models.AccountDetails
65
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&accountDetails).Error; err != nil {
66
+ return nil, err
67
+ }
68
+ return &accountDetails, nil
69
+ }
70
+
71
+ func (r *cvRepository) GetNextInitialNameNumber(ctx context.Context, gender string) (int64, error) {
72
+ sequenceName := models.GetSequenceName(gender)
73
+
74
+ var number int64
75
+ query := fmt.Sprintf("SELECT nextval('%s')", sequenceName)
76
+ err := r.db.WithContext(ctx).Raw(query).Scan(&number).Error
77
+ if err != nil {
78
+ return 0, err
79
+ }
80
+
81
+ return number, nil
82
  }
83
 
84
  func (r *cvRepository) SavePersonalityAndPreference(ctx context.Context, req *models.PersonalityAndPreferenceCV) (*models.PersonalityAndPreferenceCV, error) {
85
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
86
+ return req, err
87
+ }
88
+ return req, nil
89
  }
90
 
91
  func (r *cvRepository) GetPersonalityAndPreferenceByAccountID(ctx context.Context, accountID int64) (*models.PersonalityAndPreferenceCV, error) {
92
+ var personalityAndPreference models.PersonalityAndPreferenceCV
93
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&personalityAndPreference).Error; err != nil {
94
+ return nil, err
95
+ }
96
+ return &personalityAndPreference, nil
97
  }
98
 
99
  func (r *cvRepository) SaveFamilyMember(ctx context.Context, req *models.FamilyMemberCV) (*models.FamilyMemberCV, error) {
100
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
101
+ return req, err
102
+ }
103
+ return req, nil
104
  }
105
 
106
  func (r *cvRepository) GetFamilyMember(ctx context.Context, id int64) (*models.FamilyMemberCV, error) {
107
+ var familyMember models.FamilyMemberCV
108
+ if err := r.db.WithContext(ctx).Where("id = ?", id).First(&familyMember).Error; err != nil {
109
+ return nil, err
110
+ }
111
+ return &familyMember, nil
112
  }
113
 
114
  func (r *cvRepository) DeleteFamilyMember(ctx context.Context, id int64) error {
115
+ if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.FamilyMemberCV{}).Error; err != nil {
116
+ return err
117
+ }
118
+ return nil
119
  }
120
 
121
  func (r *cvRepository) ListFamilyMember(ctx context.Context, accountID int64) ([]models.FamilyMemberCV, error) {
122
+ familyMembers := make([]models.FamilyMemberCV, 0)
123
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&familyMembers).Error; err != nil {
124
+ return nil, err
125
+ }
126
+ return familyMembers, nil
127
  }
128
 
129
  func (r *cvRepository) SavePhysicalAndHealth(ctx context.Context, req *models.PhysicalAndHealthCV) (*models.PhysicalAndHealthCV, error) {
130
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
131
+ return req, err
132
+ }
133
+ return req, nil
134
  }
135
 
136
  func (r *cvRepository) GetPhysicalAndHealthByAccountID(ctx context.Context, accountID int64) (*models.PhysicalAndHealthCV, error) {
137
+ var physicalAndHealth models.PhysicalAndHealthCV
138
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&physicalAndHealth).Error; err != nil {
139
+ return nil, err
140
+ }
141
+ return &physicalAndHealth, nil
142
  }
143
 
144
  func (r *cvRepository) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.WorshipAndReligiousUnderstandingCV) (*models.WorshipAndReligiousUnderstandingCV, error) {
145
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
146
+ return req, err
147
+ }
148
+ return req, nil
149
  }
150
 
151
  func (r *cvRepository) GetWorshipAndReligiousUnderstandingByAccountID(ctx context.Context, accountID int64) (*models.WorshipAndReligiousUnderstandingCV, error) {
152
+ var worshipAndReligiousUnderstanding models.WorshipAndReligiousUnderstandingCV
153
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).First(&worshipAndReligiousUnderstanding).Error; err != nil {
154
+ return nil, err
155
+ }
156
+ return &worshipAndReligiousUnderstanding, nil
157
  }
158
 
159
  // SaveEducation menyimpan atau memperbarui data pendidikan ke database
160
  func (r *cvRepository) SaveEducation(ctx context.Context, req *models.EducationCV) (*models.EducationCV, error) {
161
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
162
+ return req, err
163
+ }
164
+ return req, nil
165
  }
166
 
167
  // ListEducation mengambil daftar data pendidikan berdasarkan account_id
168
  func (r *cvRepository) ListEducation(ctx context.Context, accountID int64) ([]models.EducationCV, error) {
169
+ var educations []models.EducationCV
170
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&educations).Error; err != nil {
171
+ return nil, err
172
+ }
173
+ return educations, nil
174
  }
175
 
176
  // GetEducation mengambil satu data pendidikan berdasarkan id
177
  func (r *cvRepository) GetEducation(ctx context.Context, id int64) (*models.EducationCV, error) {
178
+ var education models.EducationCV
179
+ if err := r.db.WithContext(ctx).Where("id = ?", id).First(&education).Error; err != nil {
180
+ return nil, err
181
+ }
182
+ return &education, nil
183
  }
184
 
185
  // DeleteEducation menghapus data pendidikan berdasarkan id
186
  func (r *cvRepository) DeleteEducation(ctx context.Context, id int64) error {
187
+ if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.EducationCV{}).Error; err != nil {
188
+ return err
189
+ }
190
+ return nil
191
  }
192
 
193
  // SaveJob menyimpan atau memperbarui data pekerjaan ke database
194
  func (r *cvRepository) SaveJob(ctx context.Context, req *models.JobCV) (*models.JobCV, error) {
195
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
196
+ return req, err
197
+ }
198
+ return req, nil
199
  }
200
 
201
  // ListJob mengambil daftar data pekerjaan berdasarkan account_id
202
  func (r *cvRepository) ListJob(ctx context.Context, accountID int64) ([]models.JobCV, error) {
203
+ var jobs []models.JobCV
204
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&jobs).Error; err != nil {
205
+ return nil, err
206
+ }
207
+ return jobs, nil
208
  }
209
 
210
  // GetJob mengambil satu data pekerjaan berdasarkan id
211
  func (r *cvRepository) GetJob(ctx context.Context, id int64) (*models.JobCV, error) {
212
+ var job models.JobCV
213
+ if err := r.db.WithContext(ctx).Where("id = ?", id).First(&job).Error; err != nil {
214
+ return nil, err
215
+ }
216
+ return &job, nil
217
  }
218
 
219
  // DeleteJob menghapus data pekerjaan berdasarkan id
220
  func (r *cvRepository) DeleteJob(ctx context.Context, id int64) error {
221
+ if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.JobCV{}).Error; err != nil {
222
+ return err
223
+ }
224
+ return nil
225
  }
226
 
227
  // SaveAchievement menyimpan atau memperbarui data prestasi ke database
228
  func (r *cvRepository) SaveAchievement(ctx context.Context, req *models.AchievementCV) (*models.AchievementCV, error) {
229
+ if err := r.db.WithContext(ctx).Save(req).Error; err != nil {
230
+ return req, err
231
+ }
232
+ return req, nil
233
  }
234
 
235
  // ListAchievement mengambil daftar data prestasi berdasarkan account_id
236
  func (r *cvRepository) ListAchievement(ctx context.Context, accountID int64) ([]models.AchievementCV, error) {
237
+ var achievements []models.AchievementCV
238
+ if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Find(&achievements).Error; err != nil {
239
+ return nil, err
240
+ }
241
+ return achievements, nil
242
  }
243
 
244
  // GetAchievement mengambil satu data prestasi berdasarkan id
245
  func (r *cvRepository) GetAchievement(ctx context.Context, id int64) (*models.AchievementCV, error) {
246
+ var achievement models.AchievementCV
247
+ if err := r.db.WithContext(ctx).Where("id = ?", id).First(&achievement).Error; err != nil {
248
+ return nil, err
249
+ }
250
+ return &achievement, nil
251
  }
252
 
253
  // DeleteAchievement menghapus data prestasi berdasarkan id
254
  func (r *cvRepository) DeleteAchievement(ctx context.Context, id int64) error {
255
+ if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&models.AchievementCV{}).Error; err != nil {
256
+ return err
257
+ }
258
+ return nil
259
  }
space/space/space/space/space/space/space/services/cv_service.go CHANGED
@@ -5,6 +5,7 @@ import (
5
  "errors"
6
  "math"
7
  "strconv"
 
8
 
9
  "api.qobiltu.id/models"
10
  "api.qobiltu.id/pkg/storage"
@@ -59,19 +60,19 @@ type CVService interface {
59
  type cvService struct {
60
  cvRepository repositories.CVRepository
61
  storage storage.Storage
 
62
  }
63
 
64
- func NewCVService(cvRepository repositories.CVRepository, storage storage.Storage) CVService {
65
  return &cvService{
66
  cvRepository: cvRepository,
67
  storage: storage,
 
68
  }
69
  }
70
 
71
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.SaveAccountDetailsRequest) (*models.AccountDetails, error) {
72
- // notes
73
- // jika ingin mengubah value wajib kirimkan field beserta value nya
74
- if err := validation.Validate(req); err != nil {
75
  return nil, response.HandleValidationError(err)
76
  }
77
 
@@ -81,15 +82,42 @@ func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.SaveAcco
81
  return nil, response.HandleGormError(err, "Internal Server Error")
82
  }
83
 
84
- // Apply perubahan
85
- if accountDetails == nil {
 
86
  accountDetails = &models.AccountDetails{}
 
 
 
 
 
 
 
87
  }
88
 
89
- accountDetails.AccountID = uint(req.AccountID)
 
 
 
 
 
 
 
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  utils.AssignIfNotNil(&accountDetails.FullName, req.FullName)
92
- utils.AssignIfNotNil(&accountDetails.Gender, req.Gender)
93
  utils.AssignIfNotNil(&accountDetails.DateOfBirth, req.DateOfBirth)
94
  utils.AssignIfNotNil(&accountDetails.PlaceOfBirth, req.PlaceOfBirth)
95
  utils.AssignIfNotNil(&accountDetails.Domicile, req.Domicile)
@@ -98,11 +126,11 @@ func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.SaveAcco
98
  utils.AssignIfNotNil(&accountDetails.LastJob, req.LastJob)
99
 
100
  if req.PhoneNumber != nil {
101
- sanitizedPhone := validation.SanitizePhoneNumber(*req.PhoneNumber)
102
- accountDetails.PhoneNumber = &sanitizedPhone
103
  }
104
 
105
- // Simpan data
106
  res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
107
  if err != nil {
108
  return nil, response.HandleGormError(err, "Internal Server Error")
@@ -120,7 +148,7 @@ func (s *cvService) GetAccountDetails(ctx context.Context, req *models.GetAccoun
120
  }
121
 
122
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.SavePersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
123
- if err := validation.Validate(req); err != nil {
124
  return nil, response.HandleValidationError(err)
125
  }
126
 
@@ -170,7 +198,7 @@ func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models
170
  }
171
 
172
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.CreateFamilyMemberRequest) (*models.FamilyMemberCV, error) {
173
- if err := validation.Validate(req); err != nil {
174
  return nil, response.HandleValidationError(err)
175
  }
176
 
@@ -219,7 +247,7 @@ func (s *cvService) DeleteFamilyMember(ctx context.Context, req *models.DeleteFa
219
  }
220
 
221
  func (s *cvService) UpdateFamilyMember(ctx context.Context, req *models.UpdateFamilyMemberRequest) (*models.FamilyMemberCV, error) {
222
- if err := validation.Validate(req); err != nil {
223
  return nil, response.HandleValidationError(err)
224
  }
225
 
@@ -244,7 +272,7 @@ func (s *cvService) UpdateFamilyMember(ctx context.Context, req *models.UpdateFa
244
  }
245
 
246
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.SavePhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
247
- if err := validation.Validate(req); err != nil {
248
  return nil, response.HandleValidationError(err)
249
  }
250
 
@@ -289,7 +317,7 @@ func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhy
289
  }
290
 
291
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
292
- if err := validation.Validate(req); err != nil {
293
  return nil, response.HandleValidationError(err)
294
  }
295
 
@@ -313,6 +341,7 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
313
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
314
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
315
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
 
316
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
317
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
318
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
@@ -341,7 +370,7 @@ func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req
341
  }
342
 
343
  func (s *cvService) CreateEducation(ctx context.Context, req *models.CreateEducationRequest) (*models.EducationCV, error) {
344
- if err := validation.Validate(req); err != nil {
345
  return nil, response.HandleValidationError(err)
346
  }
347
 
@@ -363,7 +392,7 @@ func (s *cvService) CreateEducation(ctx context.Context, req *models.CreateEduca
363
  }
364
 
365
  func (s *cvService) UpdateEducation(ctx context.Context, req *models.UpdateEducationRequest) (*models.EducationCV, error) {
366
- if err := validation.Validate(req); err != nil {
367
  return nil, response.HandleValidationError(err)
368
  }
369
 
@@ -402,7 +431,7 @@ func (s *cvService) DeleteEducation(ctx context.Context, req *models.DeleteEduca
402
  }
403
 
404
  func (s *cvService) CreateJob(ctx context.Context, req *models.CreateJobRequest) (*models.JobCV, error) {
405
- if err := validation.Validate(req); err != nil {
406
  return nil, response.HandleValidationError(err)
407
  }
408
 
@@ -422,7 +451,7 @@ func (s *cvService) CreateJob(ctx context.Context, req *models.CreateJobRequest)
422
  }
423
 
424
  func (s *cvService) UpdateJob(ctx context.Context, req *models.UpdateJobRequest) (*models.JobCV, error) {
425
- if err := validation.Validate(req); err != nil {
426
  return nil, response.HandleValidationError(err)
427
  }
428
 
 
5
  "errors"
6
  "math"
7
  "strconv"
8
+ "strings"
9
 
10
  "api.qobiltu.id/models"
11
  "api.qobiltu.id/pkg/storage"
 
60
  type cvService struct {
61
  cvRepository repositories.CVRepository
62
  storage storage.Storage
63
+ validator *validation.Validator
64
  }
65
 
66
+ func NewCVService(cvRepository repositories.CVRepository, storage storage.Storage, validator *validation.Validator) CVService {
67
  return &cvService{
68
  cvRepository: cvRepository,
69
  storage: storage,
70
+ validator: validator,
71
  }
72
  }
73
 
74
  func (s *cvService) SaveAccountDetails(ctx context.Context, req *models.SaveAccountDetailsRequest) (*models.AccountDetails, error) {
75
+ if err := s.validator.Validate(req); err != nil {
 
 
76
  return nil, response.HandleValidationError(err)
77
  }
78
 
 
82
  return nil, response.HandleGormError(err, "Internal Server Error")
83
  }
84
 
85
+ // Cek apakah accountDetails baru atau existing
86
+ isNew := accountDetails == nil
87
+ if isNew {
88
  accountDetails = &models.AccountDetails{}
89
+ accountDetails.AccountID = uint(req.AccountID)
90
+ }
91
+
92
+ // Simpan gender lama jika ada
93
+ oldGender := ""
94
+ if accountDetails.Gender != nil {
95
+ oldGender = strings.ToLower(*accountDetails.Gender)
96
  }
97
 
98
+ // Update gender jika dikirim dari request
99
+ var genderChanged bool
100
+ if req.Gender != nil {
101
+ newGender := strings.ToLower(*req.Gender)
102
+ if oldGender != newGender {
103
+ genderChanged = true
104
+ accountDetails.Gender = req.Gender
105
+ }
106
+ }
107
 
108
+ // Jika gender baru atau gender berubah, generate InitialName baru via sequence
109
+ if isNew || genderChanged {
110
+ if accountDetails.Gender != nil {
111
+ number, err := s.cvRepository.GetNextInitialNameNumber(ctx, *accountDetails.Gender)
112
+ if err != nil {
113
+ return nil, response.HandleGormError(err, "Gagal generate initial name")
114
+ }
115
+ accountDetails.InitialName = models.BuildInitialName(*accountDetails.Gender, number)
116
+ }
117
+ }
118
+
119
+ // Apply perubahan field lainnya
120
  utils.AssignIfNotNil(&accountDetails.FullName, req.FullName)
 
121
  utils.AssignIfNotNil(&accountDetails.DateOfBirth, req.DateOfBirth)
122
  utils.AssignIfNotNil(&accountDetails.PlaceOfBirth, req.PlaceOfBirth)
123
  utils.AssignIfNotNil(&accountDetails.Domicile, req.Domicile)
 
126
  utils.AssignIfNotNil(&accountDetails.LastJob, req.LastJob)
127
 
128
  if req.PhoneNumber != nil {
129
+ normalizedPhoneNumber := validation.NormalizePhoneNumber(*req.PhoneNumber)
130
+ accountDetails.PhoneNumber = &normalizedPhoneNumber
131
  }
132
 
133
+ // Simpan ke database
134
  res, err := s.cvRepository.SaveAccountDetails(ctx, accountDetails)
135
  if err != nil {
136
  return nil, response.HandleGormError(err, "Internal Server Error")
 
148
  }
149
 
150
  func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *models.SavePersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
151
+ if err := s.validator.Validate(req); err != nil {
152
  return nil, response.HandleValidationError(err)
153
  }
154
 
 
198
  }
199
 
200
  func (s *cvService) CreateFamilyMember(ctx context.Context, req *models.CreateFamilyMemberRequest) (*models.FamilyMemberCV, error) {
201
+ if err := s.validator.Validate(req); err != nil {
202
  return nil, response.HandleValidationError(err)
203
  }
204
 
 
247
  }
248
 
249
  func (s *cvService) UpdateFamilyMember(ctx context.Context, req *models.UpdateFamilyMemberRequest) (*models.FamilyMemberCV, error) {
250
+ if err := s.validator.Validate(req); err != nil {
251
  return nil, response.HandleValidationError(err)
252
  }
253
 
 
272
  }
273
 
274
  func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.SavePhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
275
+ if err := s.validator.Validate(req); err != nil {
276
  return nil, response.HandleValidationError(err)
277
  }
278
 
 
317
  }
318
 
319
  func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
320
+ if err := s.validator.Validate(req); err != nil {
321
  return nil, response.HandleValidationError(err)
322
  }
323
 
 
341
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
342
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
343
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
344
+ utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
345
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
346
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
347
  utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
 
370
  }
371
 
372
  func (s *cvService) CreateEducation(ctx context.Context, req *models.CreateEducationRequest) (*models.EducationCV, error) {
373
+ if err := s.validator.Validate(req); err != nil {
374
  return nil, response.HandleValidationError(err)
375
  }
376
 
 
392
  }
393
 
394
  func (s *cvService) UpdateEducation(ctx context.Context, req *models.UpdateEducationRequest) (*models.EducationCV, error) {
395
+ if err := s.validator.Validate(req); err != nil {
396
  return nil, response.HandleValidationError(err)
397
  }
398
 
 
431
  }
432
 
433
  func (s *cvService) CreateJob(ctx context.Context, req *models.CreateJobRequest) (*models.JobCV, error) {
434
+ if err := s.validator.Validate(req); err != nil {
435
  return nil, response.HandleValidationError(err)
436
  }
437
 
 
451
  }
452
 
453
  func (s *cvService) UpdateJob(ctx context.Context, req *models.UpdateJobRequest) (*models.JobCV, error) {
454
+ if err := s.validator.Validate(req); err != nil {
455
  return nil, response.HandleValidationError(err)
456
  }
457