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