add dukcapil
This commit is contained in:
@@ -1,415 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsService interface {
|
||||
GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error)
|
||||
GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error)
|
||||
}
|
||||
|
||||
type AnalyticsServiceImpl struct {
|
||||
analyticsRepo *repository.AnalyticsRepository
|
||||
}
|
||||
|
||||
func NewAnalyticsService(analyticsRepo *repository.AnalyticsRepository) *AnalyticsServiceImpl {
|
||||
return &AnalyticsServiceImpl{
|
||||
analyticsRepo: analyticsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error) {
|
||||
// Parse dates
|
||||
var startDate, endDate time.Time
|
||||
if req.StartDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.StartDate); err == nil {
|
||||
startDate = date
|
||||
}
|
||||
} else {
|
||||
// Default to last 30 days
|
||||
startDate = time.Now().AddDate(0, 0, -30)
|
||||
}
|
||||
|
||||
if req.EndDate != "" {
|
||||
if date, err := time.Parse("2006-01-02", req.EndDate); err == nil {
|
||||
endDate = date.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
}
|
||||
} else {
|
||||
endDate = time.Now()
|
||||
}
|
||||
|
||||
// Apply user context filters if not admin
|
||||
var userID *uuid.UUID
|
||||
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
if appCtx != nil && appCtx.UserRole != "admin" && appCtx.UserRole != "superadmin" {
|
||||
userID = &appCtx.UserID
|
||||
}
|
||||
|
||||
response := &contract.AnalyticsDashboardResponse{}
|
||||
|
||||
// Get summary statistics - don't filter by department for overall stats
|
||||
summaryData, err := s.analyticsRepo.GetLetterSummaryStats(ctx, startDate, endDate, userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("[DEBUG] summaryData: %v\n", summaryData)
|
||||
response.Summary = s.mapSummaryStats(summaryData)
|
||||
|
||||
// Calculate growth metrics
|
||||
response.Summary.WeekOverWeekGrowth = s.calculateWeekOverWeekGrowth(ctx)
|
||||
response.Summary.MonthOverMonthGrowth = s.calculateMonthOverMonthGrowth(ctx)
|
||||
|
||||
// Get priority distribution
|
||||
priorityData, err := s.analyticsRepo.GetPriorityDistribution(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.PriorityDistribution = s.mapPriorityDistribution(priorityData)
|
||||
|
||||
// Get department statistics
|
||||
deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.DepartmentStats = s.mapDepartmentStats(deptData)
|
||||
|
||||
// Get monthly trend (last 12 months)
|
||||
monthlyData, err := s.analyticsRepo.GetMonthlyTrendByUserID(ctx, userID, 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.MonthlyTrend = s.mapMonthlyTrend(monthlyData)
|
||||
|
||||
// Get simplified department stats (departments_stats)
|
||||
response.DepartmentsStats = s.getSimpleDepartmentStats(ctx, startDate, endDate)
|
||||
|
||||
// Get institution statistics
|
||||
instData, err := s.analyticsRepo.GetInstitutionStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.InstitutionStats = s.mapInstitutionStats(instData)
|
||||
|
||||
// Get daily activity (last 7 days)
|
||||
dailyData, err := s.analyticsRepo.GetDailyActivityByUserID(ctx, userID, 7)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.DailyActivity = s.mapDailyActivity(dailyData)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error) {
|
||||
// This would be implemented with specific queries for volume metrics
|
||||
// For now, returning a placeholder
|
||||
return &contract.LetterVolumeByTypeResponse{
|
||||
Incoming: contract.IncomingLetterVolume{
|
||||
Today: 0,
|
||||
ThisWeek: 0,
|
||||
ThisMonth: 0,
|
||||
ThisYear: 0,
|
||||
Total: 0,
|
||||
},
|
||||
Outgoing: contract.OutgoingLetterVolume{
|
||||
Today: 0,
|
||||
ThisWeek: 0,
|
||||
ThisMonth: 0,
|
||||
ThisYear: 0,
|
||||
Total: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions to map repository data to contract types
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapSummaryStats(data map[string]interface{}) contract.LetterSummaryStats {
|
||||
return contract.LetterSummaryStats{
|
||||
TotalIncoming: getInt64Value(data["total_incoming"]),
|
||||
TotalOutgoing: getInt64Value(data["total_outgoing"]),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateWeekOverWeekGrowth calculates the week over week growth rate
|
||||
func (s *AnalyticsServiceImpl) calculateWeekOverWeekGrowth(ctx context.Context) float64 {
|
||||
// Get this week's data
|
||||
thisWeekStart := time.Now().AddDate(0, 0, -int(time.Now().Weekday()))
|
||||
thisWeekEnd := time.Now()
|
||||
|
||||
// Get last week's data
|
||||
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
|
||||
lastWeekEnd := thisWeekStart.AddDate(0, 0, -1)
|
||||
|
||||
thisWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisWeekStart, thisWeekEnd, nil, nil)
|
||||
lastWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastWeekStart, lastWeekEnd, nil, nil)
|
||||
|
||||
thisWeekTotal := getInt64Value(thisWeekData["total_incoming"]) + getInt64Value(thisWeekData["total_outgoing"])
|
||||
lastWeekTotal := getInt64Value(lastWeekData["total_incoming"]) + getInt64Value(lastWeekData["total_outgoing"])
|
||||
|
||||
if lastWeekTotal > 0 {
|
||||
return float64((thisWeekTotal - lastWeekTotal) * 100 / lastWeekTotal)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// calculateMonthOverMonthGrowth calculates the month over month growth rate
|
||||
func (s *AnalyticsServiceImpl) calculateMonthOverMonthGrowth(ctx context.Context) float64 {
|
||||
// Get this month's data
|
||||
now := time.Now()
|
||||
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
thisMonthEnd := now
|
||||
|
||||
// Get last month's data
|
||||
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
|
||||
lastMonthEnd := thisMonthStart.AddDate(0, 0, -1)
|
||||
|
||||
thisMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisMonthStart, thisMonthEnd, nil, nil)
|
||||
lastMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastMonthStart, lastMonthEnd, nil, nil)
|
||||
|
||||
thisMonthTotal := getInt64Value(thisMonthData["total_incoming"]) + getInt64Value(thisMonthData["total_outgoing"])
|
||||
lastMonthTotal := getInt64Value(lastMonthData["total_incoming"]) + getInt64Value(lastMonthData["total_outgoing"])
|
||||
|
||||
if lastMonthTotal > 0 {
|
||||
return float64((thisMonthTotal - lastMonthTotal) * 100 / lastMonthTotal)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getSimpleDepartmentStats gets simplified department statistics
|
||||
func (s *AnalyticsServiceImpl) getSimpleDepartmentStats(ctx context.Context, startDate, endDate time.Time) []contract.SimpleDepartmentStats {
|
||||
// Get department stats with letter counts
|
||||
deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
return []contract.SimpleDepartmentStats{}
|
||||
}
|
||||
|
||||
result := make([]contract.SimpleDepartmentStats, 0, len(deptData))
|
||||
for _, item := range deptData {
|
||||
deptIDStr := getStringValue(item["department_id"])
|
||||
deptID, err := uuid.Parse(deptIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate total letter count (incoming + outgoing)
|
||||
letterCount := getInt64Value(item["incoming_count"]) + getInt64Value(item["outgoing_count"])
|
||||
|
||||
result = append(result, contract.SimpleDepartmentStats{
|
||||
DepartmentID: deptID,
|
||||
Department: getStringValue(item["department_name"]),
|
||||
LetterCount: letterCount,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapStatusDistribution(data []map[string]interface{}) []contract.StatusDistribution {
|
||||
result := make([]contract.StatusDistribution, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.StatusDistribution{
|
||||
Status: getStringValue(item["status"]),
|
||||
Count: getInt64Value(item["count"]),
|
||||
Percentage: getFloat64Value(item["percentage"]),
|
||||
Type: getStringValue(item["type"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapPriorityDistribution(data []map[string]interface{}) []contract.PriorityDistribution {
|
||||
result := make([]contract.PriorityDistribution, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.PriorityDistribution{
|
||||
PriorityID: getStringValue(item["priority_id"]),
|
||||
PriorityName: getStringValue(item["priority_name"]),
|
||||
Level: getIntValue(item["level"]),
|
||||
Count: getInt64Value(item["count"]),
|
||||
Percentage: getFloat64Value(item["percentage"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapDepartmentStats(data []map[string]interface{}) []contract.DepartmentStats {
|
||||
result := make([]contract.DepartmentStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if deptID, err := uuid.Parse(getStringValue(item["department_id"])); err == nil {
|
||||
result = append(result, contract.DepartmentStats{
|
||||
DepartmentID: deptID,
|
||||
DepartmentName: getStringValue(item["department_name"]),
|
||||
DepartmentCode: getStringValue(item["department_code"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
PendingCount: getInt64Value(item["pending_count"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
CompletionRate: getFloat64Value(item["completion_rate"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapMonthlyTrend(data []map[string]interface{}) []contract.MonthlyTrend {
|
||||
result := make([]contract.MonthlyTrend, 0, len(data))
|
||||
for _, item := range data {
|
||||
result = append(result, contract.MonthlyTrend{
|
||||
Month: getStringValue(item["month"]),
|
||||
Year: getIntValue(item["year"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
TotalCount: getInt64Value(item["total_count"]),
|
||||
GrowthRate: getFloat64Value(item["growth_rate"]),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapTopUsers(data []map[string]interface{}) []contract.TopUserStats {
|
||||
result := make([]contract.TopUserStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if userID, err := uuid.Parse(getStringValue(item["user_id"])); err == nil {
|
||||
result = append(result, contract.TopUserStats{
|
||||
UserID: userID,
|
||||
UserName: getStringValue(item["user_name"]),
|
||||
UserEmail: getStringValue(item["user_email"]),
|
||||
Department: getStringValue(item["department"]),
|
||||
LetterCount: getInt64Value(item["letter_count"]),
|
||||
AvgResponseTime: getFloat64Value(item["avg_response_time"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapInstitutionStats(data []map[string]interface{}) []contract.InstitutionStats {
|
||||
result := make([]contract.InstitutionStats, 0, len(data))
|
||||
for _, item := range data {
|
||||
if instID, err := uuid.Parse(getStringValue(item["institution_id"])); err == nil {
|
||||
stat := contract.InstitutionStats{
|
||||
InstitutionID: instID,
|
||||
InstitutionName: getStringValue(item["institution_name"]),
|
||||
InstitutionType: getStringValue(item["institution_type"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
TotalCount: getInt64Value(item["total_count"]),
|
||||
}
|
||||
|
||||
if lastActivity, ok := item["last_activity"].(time.Time); ok {
|
||||
stat.LastActivity = lastActivity
|
||||
}
|
||||
|
||||
result = append(result, stat)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapApprovalMetrics(data map[string]interface{}) contract.ApprovalMetrics {
|
||||
return contract.ApprovalMetrics{
|
||||
TotalSubmitted: getInt64Value(data["total_submitted"]),
|
||||
TotalApproved: getInt64Value(data["total_approved"]),
|
||||
TotalRejected: getInt64Value(data["total_rejected"]),
|
||||
TotalPending: getInt64Value(data["total_pending"]),
|
||||
ApprovalRate: getFloat64Value(data["approval_rate"]),
|
||||
RejectionRate: getFloat64Value(data["rejection_rate"]),
|
||||
AvgApprovalTime: getFloat64Value(data["avg_approval_time"]),
|
||||
AvgApprovalSteps: getFloat64Value(data["avg_approval_steps"]),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapResponseTimeStats(data map[string]interface{}) contract.ResponseTimeStats {
|
||||
return contract.ResponseTimeStats{
|
||||
MinResponseTime: getFloat64Value(data["min_response_time"]),
|
||||
MaxResponseTime: getFloat64Value(data["max_response_time"]),
|
||||
AvgResponseTime: getFloat64Value(data["avg_response_time"]),
|
||||
MedianResponseTime: getFloat64Value(data["median_response_time"]),
|
||||
P95ResponseTime: getFloat64Value(data["p95_response_time"]),
|
||||
P99ResponseTime: getFloat64Value(data["p99_response_time"]),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnalyticsServiceImpl) mapDailyActivity(data []map[string]interface{}) []contract.DailyActivity {
|
||||
result := make([]contract.DailyActivity, 0, len(data))
|
||||
for _, item := range data {
|
||||
activity := contract.DailyActivity{
|
||||
Date: "", // Leave date empty as per requirement
|
||||
DayOfWeek: getStringValue(item["day_of_week"]),
|
||||
IncomingCount: getInt64Value(item["incoming_count"]),
|
||||
OutgoingCount: getInt64Value(item["outgoing_count"]),
|
||||
}
|
||||
result = append(result, activity)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper functions to safely extract values from interface{}
|
||||
|
||||
func getStringValue(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInt64Value(v interface{}) int64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case int64:
|
||||
return val
|
||||
case float64:
|
||||
return int64(val)
|
||||
case int:
|
||||
return int64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func getIntValue(v interface{}) int {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
return val
|
||||
case int64:
|
||||
return int(val)
|
||||
case float64:
|
||||
return int(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func getFloat64Value(v interface{}) float64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case int64:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApprovalFlowService interface {
|
||||
CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||
GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||
GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||
UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||
DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error
|
||||
ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error)
|
||||
}
|
||||
|
||||
type ApprovalFlowServiceImpl struct {
|
||||
db *gorm.DB
|
||||
flowRepo *repository.ApprovalFlowRepository
|
||||
stepRepo *repository.ApprovalFlowStepRepository
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
func NewApprovalFlowService(
|
||||
db *gorm.DB,
|
||||
flowRepo *repository.ApprovalFlowRepository,
|
||||
stepRepo *repository.ApprovalFlowStepRepository,
|
||||
txManager *repository.TxManager,
|
||||
) *ApprovalFlowServiceImpl {
|
||||
return &ApprovalFlowServiceImpl{
|
||||
db: db,
|
||||
flowRepo: flowRepo,
|
||||
stepRepo: stepRepo,
|
||||
txManager: txManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error) {
|
||||
flow := &entities.ApprovalFlow{
|
||||
DepartmentID: req.DepartmentID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
|
||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
if err := s.flowRepo.Create(txCtx, flow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.Steps) > 0 {
|
||||
steps := make([]entities.ApprovalFlowStep, len(req.Steps))
|
||||
for i, stepReq := range req.Steps {
|
||||
if stepReq.ApproverRoleID == nil && stepReq.ApproverUserID == nil {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
|
||||
steps[i] = entities.ApprovalFlowStep{
|
||||
FlowID: flow.ID,
|
||||
StepOrder: stepReq.StepOrder,
|
||||
ParallelGroup: stepReq.ParallelGroup,
|
||||
ApproverRoleID: stepReq.ApproverRoleID,
|
||||
ApproverUserID: stepReq.ApproverUserID,
|
||||
Required: stepReq.Required,
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.stepRepo.CreateBulk(txCtx, steps); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.flowRepo.Get(ctx, flow.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformApprovalFlowToResponse(result), nil
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error) {
|
||||
flow, err := s.flowRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformApprovalFlowToResponse(flow), nil
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error) {
|
||||
flow, err := s.flowRepo.GetByDepartment(ctx, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformApprovalFlowToResponse(flow), nil
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error) {
|
||||
flow, err := s.flowRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flow.DepartmentID = req.DepartmentID
|
||||
flow.Name = req.Name
|
||||
flow.Description = req.Description
|
||||
flow.IsActive = req.IsActive
|
||||
|
||||
err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
if err := s.flowRepo.Update(txCtx, flow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.stepRepo.DeleteByFlow(txCtx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.Steps) > 0 {
|
||||
steps := make([]entities.ApprovalFlowStep, len(req.Steps))
|
||||
for i, stepReq := range req.Steps {
|
||||
if stepReq.ApproverRoleID == nil && stepReq.ApproverUserID == nil {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
|
||||
steps[i] = entities.ApprovalFlowStep{
|
||||
FlowID: flow.ID,
|
||||
StepOrder: stepReq.StepOrder,
|
||||
ParallelGroup: stepReq.ParallelGroup,
|
||||
ApproverRoleID: stepReq.ApproverRoleID,
|
||||
ApproverUserID: stepReq.ApproverUserID,
|
||||
Required: stepReq.Required,
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.stepRepo.CreateBulk(txCtx, steps); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.flowRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformApprovalFlowToResponse(result), nil
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error {
|
||||
return s.flowRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *ApprovalFlowServiceImpl) ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error) {
|
||||
filter := repository.ListApprovalFlowsFilter{
|
||||
DepartmentID: req.DepartmentID,
|
||||
Search: req.Search,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit to prevent performance issues
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
flows, total, err := s.flowRepo.List(ctx, filter, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]*contract.ApprovalFlowResponse, len(flows))
|
||||
for i, flow := range flows {
|
||||
items[i] = transformApprovalFlowToResponse(&flow)
|
||||
}
|
||||
|
||||
return &contract.ListApprovalFlowsResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func transformApprovalFlowToResponse(flow *entities.ApprovalFlow) *contract.ApprovalFlowResponse {
|
||||
resp := &contract.ApprovalFlowResponse{
|
||||
ID: flow.ID,
|
||||
DepartmentID: flow.DepartmentID,
|
||||
Name: flow.Name,
|
||||
Description: flow.Description,
|
||||
IsActive: flow.IsActive,
|
||||
CreatedAt: flow.CreatedAt,
|
||||
UpdatedAt: flow.UpdatedAt,
|
||||
}
|
||||
|
||||
if flow.Department != nil {
|
||||
resp.Department = &contract.DepartmentResponse{
|
||||
ID: flow.Department.ID,
|
||||
Name: flow.Department.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if len(flow.Steps) > 0 {
|
||||
resp.Steps = make([]contract.ApprovalFlowStepResponse, len(flow.Steps))
|
||||
for i, step := range flow.Steps {
|
||||
stepResp := contract.ApprovalFlowStepResponse{
|
||||
ID: step.ID,
|
||||
StepOrder: step.StepOrder,
|
||||
ParallelGroup: step.ParallelGroup,
|
||||
ApproverRoleID: step.ApproverRoleID,
|
||||
ApproverUserID: step.ApproverUserID,
|
||||
Required: step.Required,
|
||||
CreatedAt: step.CreatedAt,
|
||||
UpdatedAt: step.UpdatedAt,
|
||||
}
|
||||
|
||||
if step.ApproverRole != nil {
|
||||
stepResp.ApproverRole = &contract.RoleResponse{
|
||||
ID: step.ApproverRole.ID,
|
||||
Name: step.ApproverRole.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if step.ApproverUser != nil {
|
||||
stepResp.ApproverUser = &contract.UserResponse{
|
||||
ID: step.ApproverUser.ID,
|
||||
Name: step.ApproverUser.Name,
|
||||
Email: step.ApproverUser.Email,
|
||||
}
|
||||
}
|
||||
|
||||
resp.Steps[i] = stepResp
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"go-backend-template/internal/contract"
|
||||
"go-backend-template/internal/entities"
|
||||
"go-backend-template/internal/repository"
|
||||
"go-backend-template/internal/transformer"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
@@ -14,163 +16,101 @@ import (
|
||||
)
|
||||
|
||||
type AuthServiceImpl struct {
|
||||
userProcessor UserProcessor
|
||||
jwtSecret string
|
||||
tokenTTL time.Duration
|
||||
userRepo *repository.UserRepositoryImpl
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Roles []string `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func NewAuthService(userProcessor UserProcessor, jwtSecret string) *AuthServiceImpl {
|
||||
func NewAuthService(userRepo *repository.UserRepositoryImpl, jwtSecret string) *AuthServiceImpl {
|
||||
return &AuthServiceImpl{
|
||||
userProcessor: userProcessor,
|
||||
jwtSecret: jwtSecret,
|
||||
tokenTTL: 24 * time.Hour,
|
||||
userRepo: userRepo,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error) {
|
||||
userResponse, err := s.userProcessor.GetUserByEmail(ctx, req.Email)
|
||||
user, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
if !userResponse.IsActive {
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("user account is inactive")
|
||||
}
|
||||
|
||||
userEntity, err := s.userProcessor.GetUserEntityByEmail(ctx, req.Email)
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
token, err := s.generateToken(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(userEntity.PasswordHash), []byte(req.Password))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
|
||||
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
|
||||
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
return &contract.LoginResponse{
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
User: transformer.EntityToContract(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserResponse, error) {
|
||||
claims, err := s.parseToken(tokenString)
|
||||
func (s *AuthServiceImpl) RefreshToken(ctx context.Context, req *contract.RefreshTokenRequest) (*contract.LoginResponse, error) {
|
||||
claims, err := s.ValidateToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token: %w", err)
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
userResponse, err := s.userProcessor.GetUserByID(context.Background(), claims.UserID)
|
||||
userID, err := uuid.Parse(claims.Subject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
return nil, errors.New("invalid user ID in token")
|
||||
}
|
||||
|
||||
if !userResponse.IsActive {
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// Note: Departments are not loaded in light version, add if needed
|
||||
return userResponse, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) {
|
||||
claims, err := s.parseToken(tokenString)
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token: %w", err)
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
userResponse, err := s.userProcessor.GetUserByID(ctx, claims.UserID)
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("user account is inactive")
|
||||
}
|
||||
|
||||
token, err := s.generateToken(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !userResponse.IsActive {
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
|
||||
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
|
||||
newToken, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
return &contract.LoginResponse{
|
||||
Token: newToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
User: transformer.EntityToContract(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) Logout(ctx context.Context, tokenString string) error {
|
||||
_, err := s.parseToken(tokenString)
|
||||
func (s *AuthServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid token: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) generateToken(user *contract.UserResponse, roles []contract.RoleResponse, permissionCodes []string) (string, time.Time, error) {
|
||||
expiresAt := time.Now().Add(s.tokenTTL)
|
||||
|
||||
roleCodes := make([]string, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
roleCodes = append(roleCodes, r.Code)
|
||||
}
|
||||
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Roles: roleCodes,
|
||||
Permissions: permissionCodes,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "eslogad-be",
|
||||
Subject: user.ID.String(),
|
||||
},
|
||||
func (s *AuthServiceImpl) generateToken(user *entities.User) (string, error) {
|
||||
claims := jwt.RegisteredClaims{
|
||||
Subject: user.ID.String(),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
return tokenString, expiresAt, nil
|
||||
return token.SignedString([]byte(s.jwtSecret))
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
func (s *AuthServiceImpl) ValidateToken(tokenString string) (*jwt.RegisteredClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
|
||||
@@ -178,17 +118,13 @@ func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) ExtractAccess(tokenString string) (roles []string, permissions []string, err error) {
|
||||
claims, err := s.parseToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return claims.Roles, claims.Permissions, nil
|
||||
func (s *AuthServiceImpl) GetUserByID(ctx context.Context, userID uuid.UUID) (*entities.User, error) {
|
||||
return s.userRepo.GetByID(ctx, userID)
|
||||
}
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/repository"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DispositionRouteServiceImpl struct {
|
||||
repo *repository.DispositionRouteRepository
|
||||
}
|
||||
|
||||
func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *DispositionRouteServiceImpl {
|
||||
return &DispositionRouteServiceImpl{repo: repo}
|
||||
}
|
||||
|
||||
// CreateOrUpdate handles bulk create or update of disposition routes
|
||||
func (s *DispositionRouteServiceImpl) CreateOrUpdate(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.BulkCreateDispositionRouteResponse, error) {
|
||||
// Set default values
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
var allowedActions entities.JSONB
|
||||
if req.AllowedActions != nil {
|
||||
allowedActions = entities.JSONB(*req.AllowedActions)
|
||||
}
|
||||
|
||||
// Perform bulk upsert
|
||||
created, updated, err := s.repo.BulkUpsert(ctx, req.FromDepartmentID, req.ToDepartmentIDs, isActive, allowedActions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch all routes for the from_department_id to return
|
||||
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Transform to response
|
||||
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||
|
||||
return &contract.BulkCreateDispositionRouteResponse{
|
||||
Created: created,
|
||||
Updated: updated,
|
||||
Routes: routeResponses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create maintains backward compatibility for single route creation
|
||||
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||
// If only one to_department_id is provided, create a single route
|
||||
if len(req.ToDepartmentIDs) == 1 {
|
||||
entity := &entities.DispositionRoute{
|
||||
FromDepartmentID: req.FromDepartmentID,
|
||||
ToDepartmentID: req.ToDepartmentIDs[0],
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
} else {
|
||||
entity.IsActive = true
|
||||
}
|
||||
if req.AllowedActions != nil {
|
||||
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||
}
|
||||
|
||||
// Use upsert to handle create or update
|
||||
if err := s.repo.Upsert(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch the created/updated route
|
||||
route, err := s.repo.Get(ctx, entity.ID)
|
||||
if err != nil {
|
||||
// If we can't get by ID (new creation), try to get by from/to combination
|
||||
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range routes {
|
||||
if r.ToDepartmentID == req.ToDepartmentIDs[0] {
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{r})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*route})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// For multiple to_department_ids, use bulk create/update
|
||||
bulkResp, err := s.CreateOrUpdate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the first route as response for backward compatibility
|
||||
if len(bulkResp.Routes) > 0 {
|
||||
return &bulkResp.Routes[0], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||
entity, err := s.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
}
|
||||
if req.AllowedActions != nil {
|
||||
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||
}
|
||||
if err := s.repo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *DispositionRouteServiceImpl) Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error) {
|
||||
entity, err := s.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error) {
|
||||
list, err := s.repo.ListByFromDept(ctx, from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListDispositionRoutesResponse{Routes: transformer.DispositionRoutesToContract(list)}, nil
|
||||
}
|
||||
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
|
||||
return s.repo.SetActive(ctx, id, active)
|
||||
}
|
||||
|
||||
// ListGrouped returns all disposition routes grouped by from_department_id with clean department structure
|
||||
func (s *DispositionRouteServiceImpl) ListGrouped(ctx context.Context) (*contract.ListDispositionRoutesGroupedResponse, error) {
|
||||
// Get routes with department details
|
||||
routes, err := s.repo.ListAllGroupedWithDepartments(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Group routes by from_department_id and collect department info
|
||||
type groupedData struct {
|
||||
fromDept contract.DepartmentMapping
|
||||
toDepts []contract.DepartmentMapping
|
||||
toDeptMap map[uuid.UUID]bool // To avoid duplicates
|
||||
}
|
||||
|
||||
grouped := make(map[uuid.UUID]*groupedData)
|
||||
|
||||
for _, route := range routes {
|
||||
if _, exists := grouped[route.FromDepartmentID]; !exists {
|
||||
grouped[route.FromDepartmentID] = &groupedData{
|
||||
fromDept: contract.DepartmentMapping{
|
||||
ID: route.FromDepartmentID,
|
||||
Name: route.FromDepartment.Name,
|
||||
},
|
||||
toDepts: []contract.DepartmentMapping{},
|
||||
toDeptMap: make(map[uuid.UUID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Add to_department if not already added (avoid duplicates)
|
||||
if !grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] {
|
||||
grouped[route.FromDepartmentID].toDepts = append(
|
||||
grouped[route.FromDepartmentID].toDepts,
|
||||
contract.DepartmentMapping{
|
||||
ID: route.ToDepartmentID,
|
||||
Name: route.ToDepartment.Name,
|
||||
},
|
||||
)
|
||||
grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
var dispositions []contract.DispositionRouteGroupedItem
|
||||
for _, data := range grouped {
|
||||
dispositions = append(dispositions, contract.DispositionRouteGroupedItem{
|
||||
FromDepartment: data.fromDept,
|
||||
ToDepartments: data.toDepts,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by from department name for consistent ordering
|
||||
sort.Slice(dispositions, func(i, j int) bool {
|
||||
return dispositions[i].FromDepartment.Name < dispositions[j].FromDepartment.Name
|
||||
})
|
||||
|
||||
return &contract.ListDispositionRoutesGroupedResponse{
|
||||
Dispositions: dispositions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListAll returns all disposition routes with department details
|
||||
func (s *DispositionRouteServiceImpl) ListAll(ctx context.Context) (*contract.ListDispositionRoutesDetailedResponse, error) {
|
||||
routes, err := s.repo.ListAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||
|
||||
return &contract.ListDispositionRoutesDetailedResponse{
|
||||
Routes: routeResponses,
|
||||
Total: len(routeResponses),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"go-backend-template/internal/client"
|
||||
"go-backend-template/internal/contract"
|
||||
)
|
||||
|
||||
type DukcapilService interface {
|
||||
FaceMatch(ctx context.Context, req *contract.FaceMatchRequest) (*contract.FaceMatchResponse, error)
|
||||
}
|
||||
|
||||
type DukcapilServiceImpl struct {
|
||||
client *client.DukcapilClient
|
||||
}
|
||||
|
||||
func NewDukcapilService(c *client.DukcapilClient) *DukcapilServiceImpl {
|
||||
return &DukcapilServiceImpl{client: c}
|
||||
}
|
||||
|
||||
func (s *DukcapilServiceImpl) FaceMatch(ctx context.Context, req *contract.FaceMatchRequest) (*contract.FaceMatchResponse, error) {
|
||||
upstream, err := s.client.FaceMatch(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matches := parseFaceMatches(upstream.Response)
|
||||
|
||||
return &contract.FaceMatchResponse{
|
||||
TID: upstream.TID,
|
||||
ErrorCode: upstream.ErrorCode,
|
||||
Error: upstream.Error,
|
||||
RequestType: upstream.RequestType,
|
||||
Threshold: upstream.FaceThreshold,
|
||||
MaxResults: upstream.MaxResults,
|
||||
Matches: matches,
|
||||
Raw: upstream,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseFaceMatches decodes the nested string field
|
||||
// `{"face":{"FACE_T5":{"<NIK>":<score>, ...}}}` into a slice of results.
|
||||
func parseFaceMatches(raw string) []contract.FaceMatchResult {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var envelope struct {
|
||||
Face map[string]map[string]json.Number `json:"face"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &envelope); err != nil {
|
||||
return nil
|
||||
}
|
||||
results := make([]contract.FaceMatchResult, 0)
|
||||
for _, group := range envelope.Face {
|
||||
for nik, scoreNum := range group {
|
||||
score, err := strconv.ParseFloat(scoreNum.String(), 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, contract.FaceMatchResult{NIK: nik, Score: score})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Compile-time assertion.
|
||||
var _ DukcapilService = (*DukcapilServiceImpl)(nil)
|
||||
@@ -1,109 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type FileStorage interface {
|
||||
Upload(ctx context.Context, bucket, key string, content []byte, contentType string) (string, error)
|
||||
EnsureBucket(ctx context.Context, bucket string) error
|
||||
}
|
||||
|
||||
type FileServiceImpl struct {
|
||||
storage FileStorage
|
||||
userProcessor UserProcessor
|
||||
profileBucket string
|
||||
docBucket string
|
||||
finalBucket string
|
||||
}
|
||||
|
||||
func NewFileService(storage FileStorage, userProcessor UserProcessor, profileBucket, docBucket string, finalBucket string) *FileServiceImpl {
|
||||
return &FileServiceImpl{storage: storage, userProcessor: userProcessor, profileBucket: profileBucket, docBucket: docBucket, finalBucket: finalBucket}
|
||||
}
|
||||
|
||||
func (s *FileServiceImpl) UploadProfileAvatar(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, error) {
|
||||
if err := s.storage.EnsureBucket(ctx, s.profileBucket); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||
ext = mimeExt
|
||||
}
|
||||
key := buildObjectKey("profile", userID, ext)
|
||||
url, err := s.storage.Upload(ctx, s.profileBucket, key, content, contentType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, _ = s.userProcessor.UpdateUserProfile(ctx, userID, &contract.UpdateUserProfileRequest{AvatarURL: &url})
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (s *FileServiceImpl) UploadDocument(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) {
|
||||
if err := s.storage.EnsureBucket(ctx, s.docBucket); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||
ext = mimeExt
|
||||
}
|
||||
key := buildObjectKey("documents", userID, ext)
|
||||
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return url, key, nil
|
||||
}
|
||||
|
||||
func (s *FileServiceImpl) UploadDocumentFinal(ctx context.Context, userID uuid.UUID, filename string, content []byte, contentType string) (string, string, error) {
|
||||
if err := s.storage.EnsureBucket(ctx, s.docBucket); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||
ext = mimeExt
|
||||
}
|
||||
key := buildObjectKey("finals", userID, ext)
|
||||
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return url, key, nil
|
||||
}
|
||||
|
||||
func buildObjectKey(prefix string, userID uuid.UUID, ext string) string {
|
||||
now := time.Now().UTC()
|
||||
parts := []string{
|
||||
prefix,
|
||||
userID.String(),
|
||||
now.Format("2006/01/02"),
|
||||
uuid.New().String(),
|
||||
}
|
||||
key := strings.Join(parts, "/")
|
||||
if ext != "" {
|
||||
key += "." + ext
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func mimeExtFromContentType(ct string) string {
|
||||
switch strings.ToLower(ct) {
|
||||
case "image/jpeg", "image/jpg":
|
||||
return "jpg"
|
||||
case "image/png":
|
||||
return "png"
|
||||
case "image/webp":
|
||||
return "webp"
|
||||
case "application/pdf":
|
||||
return "pdf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,633 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"eslogad-be/config"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/repository"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MasterServiceImpl struct {
|
||||
labelRepo *repository.LabelRepository
|
||||
priorityRepo *repository.PriorityRepository
|
||||
institutionRepo *repository.InstitutionRepository
|
||||
dispRepo *repository.DispositionActionRepository
|
||||
departmentRepo *repository.DepartmentRepository
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository, department *repository.DepartmentRepository, cfg *config.Config) *MasterServiceImpl {
|
||||
return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp, departmentRepo: department, config: cfg}
|
||||
}
|
||||
|
||||
// Labels
|
||||
func (s *MasterServiceImpl) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
|
||||
entity := &entities.Label{Name: req.Name, Color: req.Color}
|
||||
if err := s.labelRepo.Create(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.LabelsToContract([]entities.Label{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
|
||||
entity := &entities.Label{ID: id}
|
||||
if req.Name != nil {
|
||||
entity.Name = *req.Name
|
||||
}
|
||||
if req.Color != nil {
|
||||
entity.Color = req.Color
|
||||
}
|
||||
if err := s.labelRepo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := s.labelRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.LabelsToContract([]entities.Label{*e})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) DeleteLabel(ctx context.Context, id uuid.UUID) error {
|
||||
return s.labelRepo.Delete(ctx, id)
|
||||
}
|
||||
func (s *MasterServiceImpl) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
|
||||
list, err := s.labelRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListLabelsResponse{Labels: transformer.LabelsToContract(list)}, nil
|
||||
}
|
||||
|
||||
// Priorities
|
||||
func (s *MasterServiceImpl) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||
entity := &entities.Priority{Name: req.Name, Level: req.Level}
|
||||
if err := s.priorityRepo.Create(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.PrioritiesToContract([]entities.Priority{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||
entity := &entities.Priority{ID: id}
|
||||
if req.Name != nil {
|
||||
entity.Name = *req.Name
|
||||
}
|
||||
if req.Level != nil {
|
||||
entity.Level = *req.Level
|
||||
}
|
||||
if err := s.priorityRepo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := s.priorityRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.PrioritiesToContract([]entities.Priority{*e})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) DeletePriority(ctx context.Context, id uuid.UUID) error {
|
||||
return s.priorityRepo.Delete(ctx, id)
|
||||
}
|
||||
func (s *MasterServiceImpl) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
|
||||
list, err := s.priorityRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListPrioritiesResponse{Priorities: transformer.PrioritiesToContract(list)}, nil
|
||||
}
|
||||
|
||||
// Institutions
|
||||
func (s *MasterServiceImpl) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||
entity := &entities.Institution{Name: req.Name, Type: entities.InstitutionType(req.Type), Address: req.Address, ContactPerson: req.ContactPerson, Phone: req.Phone, Email: req.Email}
|
||||
if err := s.institutionRepo.Create(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.InstitutionsToContract([]entities.Institution{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||
entity := &entities.Institution{ID: id}
|
||||
if req.Name != nil {
|
||||
entity.Name = *req.Name
|
||||
}
|
||||
if req.Type != nil {
|
||||
entity.Type = entities.InstitutionType(*req.Type)
|
||||
}
|
||||
if req.Address != nil {
|
||||
entity.Address = req.Address
|
||||
}
|
||||
if req.ContactPerson != nil {
|
||||
entity.ContactPerson = req.ContactPerson
|
||||
}
|
||||
if req.Phone != nil {
|
||||
entity.Phone = req.Phone
|
||||
}
|
||||
if req.Email != nil {
|
||||
entity.Email = req.Email
|
||||
}
|
||||
if err := s.institutionRepo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := s.institutionRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.InstitutionsToContract([]entities.Institution{*e})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
||||
return s.institutionRepo.Delete(ctx, id)
|
||||
}
|
||||
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
|
||||
list, err := s.institutionRepo.ListWithSearch(ctx, req.Search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListInstitutionsResponse{Institutions: transformer.InstitutionsToContract(list)}, nil
|
||||
}
|
||||
|
||||
// Disposition Actions
|
||||
func (s *MasterServiceImpl) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||
entity := &entities.DispositionAction{Code: req.Code, Label: req.Label, Description: req.Description}
|
||||
if req.RequiresNote != nil {
|
||||
entity.RequiresNote = *req.RequiresNote
|
||||
}
|
||||
if req.GroupName != nil {
|
||||
entity.GroupName = req.GroupName
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
entity.SortOrder = req.SortOrder
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
}
|
||||
if err := s.dispRepo.Create(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*entity})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||
entity := &entities.DispositionAction{ID: id}
|
||||
if req.Code != nil {
|
||||
entity.Code = *req.Code
|
||||
}
|
||||
if req.Label != nil {
|
||||
entity.Label = *req.Label
|
||||
}
|
||||
if req.Description != nil {
|
||||
entity.Description = req.Description
|
||||
}
|
||||
if req.RequiresNote != nil {
|
||||
entity.RequiresNote = *req.RequiresNote
|
||||
}
|
||||
if req.GroupName != nil {
|
||||
entity.GroupName = req.GroupName
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
entity.SortOrder = req.SortOrder
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
}
|
||||
if err := s.dispRepo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := s.dispRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*e})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *MasterServiceImpl) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
|
||||
return s.dispRepo.Delete(ctx, id)
|
||||
}
|
||||
func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
|
||||
list, err := s.dispRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListDispositionActionsResponse{Actions: transformer.DispositionActionsToContract(list)}, nil
|
||||
}
|
||||
|
||||
// Departments
|
||||
func (s *MasterServiceImpl) CreateDepartment(ctx context.Context, req *contract.CreateDepartmentRequest) (*contract.GetDepartmentResponse, error) {
|
||||
// Build the path based on parent
|
||||
var path string
|
||||
if req.ParentID != nil {
|
||||
// Get parent department to build the path
|
||||
parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Build path as parent.path + code
|
||||
path = parent.Path + "." + req.Code
|
||||
} else {
|
||||
// Root level department, just use the code as path
|
||||
path = req.Code
|
||||
}
|
||||
|
||||
entity := &entities.Department{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Path: path,
|
||||
}
|
||||
if err := s.departmentRepo.Create(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get parent name if parent exists
|
||||
var parentName *string
|
||||
if req.ParentID != nil {
|
||||
if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil {
|
||||
parentName = &parent.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.GetDepartmentResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
Code: entity.Code,
|
||||
Path: entity.Path,
|
||||
ParentID: req.ParentID,
|
||||
ParentName: parentName,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) GetDepartment(ctx context.Context, id uuid.UUID) (*contract.GetDepartmentResponse, error) {
|
||||
entity, err := s.departmentRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Derive parent_id and parent_name from path
|
||||
var parentID *uuid.UUID
|
||||
var parentName *string
|
||||
parts := strings.Split(entity.Path, ".")
|
||||
if len(parts) > 1 {
|
||||
// Has parent, try to find it
|
||||
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||
if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil {
|
||||
parentID = &parent.ID
|
||||
parentName = &parent.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.GetDepartmentResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
Code: entity.Code,
|
||||
Path: entity.Path,
|
||||
ParentID: parentID,
|
||||
ParentName: parentName,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) UpdateDepartment(ctx context.Context, id uuid.UUID, req *contract.UpdateDepartmentRequest) (*contract.GetDepartmentResponse, error) {
|
||||
entity, err := s.departmentRepo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the old path before changes
|
||||
oldPath := entity.Path
|
||||
|
||||
if req.Name != nil {
|
||||
entity.Name = *req.Name
|
||||
}
|
||||
if req.Code != nil {
|
||||
entity.Code = *req.Code
|
||||
}
|
||||
|
||||
// Rebuild path if parent is being changed or code is being changed
|
||||
if req.ParentID != nil || req.Code != nil {
|
||||
// Determine the code to use (new code if provided, otherwise existing)
|
||||
code := entity.Code
|
||||
if req.Code != nil {
|
||||
code = *req.Code
|
||||
}
|
||||
|
||||
// Build the new path based on parent
|
||||
var path string
|
||||
if req.ParentID != nil {
|
||||
if *req.ParentID == uuid.Nil {
|
||||
// Moving to root level
|
||||
path = code
|
||||
} else {
|
||||
// Get parent department to build the path
|
||||
parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Build path as parent.path + code
|
||||
path = parent.Path + "." + code
|
||||
}
|
||||
} else if req.Code != nil {
|
||||
// Code changed but parent not specified, rebuild path with current parent
|
||||
// Extract parent path from current path
|
||||
parts := strings.Split(entity.Path, ".")
|
||||
if len(parts) > 1 {
|
||||
// Has parent, rebuild with new code
|
||||
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||
path = parentPath + "." + code
|
||||
} else {
|
||||
// Root level, just use new code
|
||||
path = code
|
||||
}
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
entity.Path = path
|
||||
}
|
||||
}
|
||||
|
||||
// Update the department
|
||||
if err := s.departmentRepo.Update(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the path changed, update all children paths
|
||||
if oldPath != entity.Path {
|
||||
if err := s.departmentRepo.UpdateChildrenPaths(ctx, oldPath, entity.Path); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
// You might want to handle this differently based on your requirements
|
||||
// For now, we'll continue since the parent update succeeded
|
||||
}
|
||||
}
|
||||
|
||||
// Derive parent_id and parent_name from path for response
|
||||
var parentID *uuid.UUID
|
||||
var parentName *string
|
||||
if req.ParentID != nil {
|
||||
parentID = req.ParentID
|
||||
// Get parent name
|
||||
if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil {
|
||||
parentName = &parent.Name
|
||||
}
|
||||
} else {
|
||||
// Derive from path if not provided in request
|
||||
parts := strings.Split(entity.Path, ".")
|
||||
if len(parts) > 1 {
|
||||
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||
if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil {
|
||||
parentID = &parent.ID
|
||||
parentName = &parent.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.GetDepartmentResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
Code: entity.Code,
|
||||
Path: entity.Path,
|
||||
ParentID: parentID,
|
||||
ParentName: parentName,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) DeleteDepartment(ctx context.Context, id uuid.UUID) error {
|
||||
return s.departmentRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) GetOrganizationalChartByID(ctx context.Context, departmentID uuid.UUID) (*contract.OrganizationalChartResponse, error) {
|
||||
// First get the department to find its path
|
||||
department, err := s.departmentRepo.Get(ctx, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now get the organizational chart starting from this department's path
|
||||
return s.GetOrganizationalChart(ctx, department.Path)
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) GetOrganizationalChart(ctx context.Context, rootPath string) (*contract.OrganizationalChartResponse, error) {
|
||||
var departments []entities.Department
|
||||
var err error
|
||||
|
||||
// Get config values
|
||||
parentPath := s.config.Department.ParentPath
|
||||
excludedPaths := s.config.Department.ExcludedPaths
|
||||
|
||||
if rootPath == "" {
|
||||
// Get all departments with parent filter
|
||||
departments, err = s.departmentRepo.GetAllWithParentFilter(ctx, parentPath, excludedPaths)
|
||||
} else {
|
||||
// Get departments under specific path
|
||||
departments, err = s.departmentRepo.GetByPathPrefix(ctx, rootPath)
|
||||
// Filter out excluded paths manually for specific path queries
|
||||
filteredDepts := make([]entities.Department, 0)
|
||||
for _, dept := range departments {
|
||||
excluded := false
|
||||
for _, excludedPath := range excludedPaths {
|
||||
if strings.Contains(dept.Path, excludedPath) {
|
||||
excluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !excluded {
|
||||
filteredDepts = append(filteredDepts, dept)
|
||||
}
|
||||
}
|
||||
departments = filteredDepts
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build the tree structure
|
||||
nodeMap := make(map[string]*contract.DepartmentNode)
|
||||
roots := make([]*contract.DepartmentNode, 0)
|
||||
|
||||
// Calculate base level offset based on parent path
|
||||
baseLevelOffset := 0
|
||||
if parentPath != "" {
|
||||
baseLevelOffset = len(strings.Split(parentPath, ".")) - 1
|
||||
}
|
||||
|
||||
// First pass: create all nodes including missing parents
|
||||
for _, dept := range departments {
|
||||
pathParts := strings.Split(dept.Path, ".")
|
||||
|
||||
// Create any missing parent nodes
|
||||
for i := 1; i <= len(pathParts); i++ {
|
||||
currentPath := strings.Join(pathParts[:i], ".")
|
||||
if _, exists := nodeMap[currentPath]; !exists {
|
||||
// Calculate level for this path
|
||||
adjustedLevel := i - baseLevelOffset
|
||||
if adjustedLevel < 1 {
|
||||
adjustedLevel = 1
|
||||
}
|
||||
|
||||
// Create node (placeholder for missing parents, real data for existing)
|
||||
var node *contract.DepartmentNode
|
||||
if currentPath == dept.Path {
|
||||
// This is the actual department
|
||||
node = &contract.DepartmentNode{
|
||||
ID: dept.ID,
|
||||
Name: dept.Name,
|
||||
Code: dept.Code,
|
||||
Path: dept.Path,
|
||||
Level: adjustedLevel,
|
||||
Children: make([]*contract.DepartmentNode, 0),
|
||||
}
|
||||
} else {
|
||||
// This is a missing parent - create placeholder
|
||||
// Extract the last segment as the name
|
||||
lastSegment := pathParts[i-1]
|
||||
node = &contract.DepartmentNode{
|
||||
ID: uuid.Nil, // Use nil UUID for placeholder
|
||||
Name: strings.ToUpper(strings.ReplaceAll(lastSegment, "_", " ")),
|
||||
Code: lastSegment,
|
||||
Path: currentPath,
|
||||
Level: adjustedLevel,
|
||||
Children: make([]*contract.DepartmentNode, 0),
|
||||
}
|
||||
}
|
||||
nodeMap[currentPath] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: build the tree relationships
|
||||
// Only process nodes that actually exist in the database (not placeholders)
|
||||
processedPaths := make(map[string]bool)
|
||||
for _, dept := range departments {
|
||||
if processedPaths[dept.Path] {
|
||||
continue
|
||||
}
|
||||
processedPaths[dept.Path] = true
|
||||
|
||||
node := nodeMap[dept.Path]
|
||||
pathParts := strings.Split(dept.Path, ".")
|
||||
|
||||
// Check if this should be a root node
|
||||
isRoot := false
|
||||
if rootPath != "" && dept.Path == rootPath {
|
||||
// Explicitly requested root
|
||||
isRoot = true
|
||||
} else if rootPath == "" && parentPath != "" && dept.Path == parentPath {
|
||||
// The configured parent path is the root when showing all
|
||||
isRoot = true
|
||||
} else if len(pathParts) == 1 {
|
||||
// Single segment path
|
||||
isRoot = true
|
||||
} else {
|
||||
// Find parent path
|
||||
parentPathStr := strings.Join(pathParts[:len(pathParts)-1], ".")
|
||||
if parent, exists := nodeMap[parentPathStr]; exists {
|
||||
// Check if this child is already added
|
||||
alreadyAdded := false
|
||||
for _, child := range parent.Children {
|
||||
if child.Path == node.Path {
|
||||
alreadyAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyAdded {
|
||||
parent.Children = append(parent.Children, node)
|
||||
}
|
||||
} else {
|
||||
// Parent doesn't exist - this is an orphaned node
|
||||
// Only include it as a root if it's a direct child of the parent path
|
||||
if parentPath != "" {
|
||||
// Check if this is a direct child of the configured parent
|
||||
expectedParent := parentPath
|
||||
actualParent := strings.Join(pathParts[:len(pathParts)-1], ".")
|
||||
if actualParent != expectedParent {
|
||||
// This is an orphaned node - skip it
|
||||
continue
|
||||
}
|
||||
}
|
||||
isRoot = true
|
||||
}
|
||||
}
|
||||
|
||||
if isRoot {
|
||||
// Check for duplicates in roots
|
||||
alreadyInRoots := false
|
||||
for _, r := range roots {
|
||||
if r.Path == node.Path {
|
||||
alreadyInRoots = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyInRoots {
|
||||
roots = append(roots, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort children at each level
|
||||
var sortChildren func([]*contract.DepartmentNode)
|
||||
sortChildren = func(nodes []*contract.DepartmentNode) {
|
||||
for _, node := range nodes {
|
||||
if len(node.Children) > 0 {
|
||||
// Sort children by name
|
||||
sort.Slice(node.Children, func(i, j int) bool {
|
||||
return node.Children[i].Name < node.Children[j].Name
|
||||
})
|
||||
sortChildren(node.Children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort root nodes
|
||||
sort.Slice(roots, func(i, j int) bool {
|
||||
return roots[i].Name < roots[j].Name
|
||||
})
|
||||
sortChildren(roots)
|
||||
|
||||
return &contract.OrganizationalChartResponse{
|
||||
Chart: roots,
|
||||
TotalNodes: len(departments),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) {
|
||||
// Set default values if not provided
|
||||
page := req.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := req.Limit
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit to prevent performance issues
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Use filtered list with parent path from config
|
||||
parentPath := s.config.Department.ParentPath
|
||||
excludedPaths := s.config.Department.ExcludedPaths
|
||||
|
||||
list, total, err := s.departmentRepo.ListWithParentFilter(ctx, req.Search, limit, offset, parentPath, excludedPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.ListDepartmentsResponse{
|
||||
Departments: transformer.DepartmentsToContract(list),
|
||||
Total: total,
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"eslogad-be/internal/config"
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
novu "github.com/novuhq/go-novu/lib"
|
||||
)
|
||||
|
||||
type NotificationService interface {
|
||||
TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error)
|
||||
BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error)
|
||||
GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error)
|
||||
UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error)
|
||||
}
|
||||
|
||||
type NotificationServiceImpl struct {
|
||||
client *novu.APIClient
|
||||
config *config.NovuConfig
|
||||
userProcessor UserProcessorForNotification
|
||||
}
|
||||
|
||||
type UserProcessorForNotification interface {
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
}
|
||||
|
||||
func NewNotificationService(cfg *config.NovuConfig, userProcessor UserProcessorForNotification) *NotificationServiceImpl {
|
||||
var client *novu.APIClient
|
||||
if cfg.APIKey != "" {
|
||||
// Create Novu config with backend URL
|
||||
novuConfig := &novu.Config{}
|
||||
if cfg.BaseURL != "" {
|
||||
backendURL, err := url.Parse(cfg.BaseURL)
|
||||
if err == nil {
|
||||
novuConfig.BackendURL = backendURL
|
||||
}
|
||||
}
|
||||
client = novu.NewAPIClient(cfg.APIKey, novuConfig)
|
||||
}
|
||||
|
||||
return &NotificationServiceImpl{
|
||||
client: client,
|
||||
config: cfg,
|
||||
userProcessor: userProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: "notification service not configured",
|
||||
}, nil
|
||||
}
|
||||
|
||||
subscriberID := req.UserID.String()
|
||||
|
||||
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prepare the trigger payload
|
||||
to := map[string]interface{}{
|
||||
"subscriberId": subscriberID,
|
||||
}
|
||||
|
||||
// Add additional recipient information if provided
|
||||
if req.To != nil {
|
||||
if req.To.Email != "" {
|
||||
to["email"] = req.To.Email
|
||||
}
|
||||
if req.To.Phone != "" {
|
||||
to["phone"] = req.To.Phone
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare overrides
|
||||
overrides := make(map[string]interface{})
|
||||
if req.Overrides != nil {
|
||||
if req.Overrides.Email != nil {
|
||||
overrides["email"] = req.Overrides.Email
|
||||
}
|
||||
if req.Overrides.SMS != nil {
|
||||
overrides["sms"] = req.Overrides.SMS
|
||||
}
|
||||
if req.Overrides.InApp != nil {
|
||||
overrides["in_app"] = req.Overrides.InApp
|
||||
}
|
||||
if req.Overrides.Push != nil {
|
||||
overrides["push"] = req.Overrides.Push
|
||||
}
|
||||
if req.Overrides.Chat != nil {
|
||||
overrides["chat"] = req.Overrides.Chat
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the notification using the template ID
|
||||
triggerPayload := novu.ITriggerPayloadOptions{
|
||||
To: to,
|
||||
Payload: req.TemplateData,
|
||||
Overrides: overrides,
|
||||
}
|
||||
|
||||
resp, err := s.client.EventApi.Trigger(ctx, req.TemplateID, triggerPayload)
|
||||
if err != nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to trigger notification: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Extract transaction ID from response
|
||||
transactionID := ""
|
||||
if respData, ok := resp.Data.(map[string]interface{}); ok {
|
||||
if txID, exists := respData["transactionId"]; exists {
|
||||
transactionID = fmt.Sprintf("%v", txID)
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: true,
|
||||
TransactionID: transactionID,
|
||||
Message: "notification triggered successfully",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.BulkTriggerNotificationResponse{
|
||||
Success: false,
|
||||
TotalSent: 0,
|
||||
TotalFailed: len(req.UserIDs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
results := make([]contract.NotificationResult, 0, len(req.UserIDs))
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, userID := range req.UserIDs {
|
||||
// Create individual trigger request
|
||||
triggerReq := &contract.TriggerNotificationRequest{
|
||||
UserID: userID,
|
||||
TemplateID: req.TemplateID,
|
||||
TemplateData: req.TemplateData,
|
||||
Overrides: req.Overrides,
|
||||
}
|
||||
|
||||
resp, err := s.TriggerNotification(ctx, triggerReq)
|
||||
|
||||
result := contract.NotificationResult{
|
||||
UserID: userID,
|
||||
Success: resp.Success,
|
||||
}
|
||||
|
||||
if resp.Success {
|
||||
result.TransactionID = resp.TransactionID
|
||||
successCount++
|
||||
} else {
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.Error = resp.Message
|
||||
}
|
||||
failedCount++
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return &contract.BulkTriggerNotificationResponse{
|
||||
Success: failedCount == 0,
|
||||
TotalSent: successCount,
|
||||
TotalFailed: failedCount,
|
||||
Results: results,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error) {
|
||||
if s.client == nil {
|
||||
return nil, fmt.Errorf("notification service not configured")
|
||||
}
|
||||
|
||||
subscriberID := userID.String()
|
||||
|
||||
// Try to get the subscriber
|
||||
subscriber, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err != nil {
|
||||
// If subscriber doesn't exist, create it
|
||||
_, createErr := s.ensureSubscriberExists(ctx, userID)
|
||||
if createErr != nil {
|
||||
return nil, fmt.Errorf("failed to get or create subscriber: %w", createErr)
|
||||
}
|
||||
|
||||
// Try to get again after creation
|
||||
subscriber, err = s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get subscriber after creation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Novu subscriber to our response format
|
||||
response := &contract.GetSubscriberResponse{
|
||||
SubscriberID: subscriberID,
|
||||
}
|
||||
|
||||
if subData, ok := subscriber.Data.(map[string]interface{}); ok {
|
||||
if email, exists := subData["email"]; exists {
|
||||
response.Email = fmt.Sprintf("%v", email)
|
||||
}
|
||||
if firstName, exists := subData["firstName"]; exists {
|
||||
response.FirstName = fmt.Sprintf("%v", firstName)
|
||||
}
|
||||
if lastName, exists := subData["lastName"]; exists {
|
||||
response.LastName = fmt.Sprintf("%v", lastName)
|
||||
}
|
||||
if phone, exists := subData["phone"]; exists {
|
||||
response.Phone = fmt.Sprintf("%v", phone)
|
||||
}
|
||||
if avatar, exists := subData["avatar"]; exists {
|
||||
response.Avatar = fmt.Sprintf("%v", avatar)
|
||||
}
|
||||
if data, exists := subData["data"]; exists {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
response.Data = dataMap
|
||||
}
|
||||
}
|
||||
if channels, exists := subData["channels"]; exists {
|
||||
if channelList, ok := channels.([]interface{}); ok {
|
||||
response.Channels = make([]contract.ChannelCredentials, 0, len(channelList))
|
||||
for _, ch := range channelList {
|
||||
if chMap, ok := ch.(map[string]interface{}); ok {
|
||||
channelCred := contract.ChannelCredentials{}
|
||||
if chType, exists := chMap["providerId"]; exists {
|
||||
channelCred.Channel = fmt.Sprintf("%v", chType)
|
||||
}
|
||||
if creds, exists := chMap["credentials"]; exists {
|
||||
if credMap, ok := creds.(map[string]interface{}); ok {
|
||||
channelCred.Credentials = credMap
|
||||
}
|
||||
}
|
||||
response.Channels = append(response.Channels, channelCred)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: "notification service not configured",
|
||||
}, nil
|
||||
}
|
||||
|
||||
subscriberID := req.UserID.String()
|
||||
|
||||
// Ensure subscriber exists
|
||||
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Since the Novu Go SDK doesn't have UpdateCredentials, we'll update the subscriber data instead
|
||||
// Get user info to update subscriber
|
||||
user, err := s.userProcessor.GetUserByID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to get user data: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prepare subscriber data with channel credentials stored in data
|
||||
data := map[string]interface{}{
|
||||
"userId": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"isActive": user.IsActive,
|
||||
"updatedAt": user.UpdatedAt,
|
||||
}
|
||||
|
||||
// Store channel credentials in the subscriber data
|
||||
channelKey := fmt.Sprintf("channel_%s", req.Channel)
|
||||
data[channelKey] = req.Credentials
|
||||
|
||||
// Update subscriber with new data
|
||||
updateData := novu.SubscriberPayload{
|
||||
Email: user.Email,
|
||||
FirstName: user.Name,
|
||||
LastName: "",
|
||||
Phone: "",
|
||||
Avatar: "",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
_, err = s.client.SubscriberApi.Update(ctx, subscriberID, updateData)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to update subscriber channel: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: true,
|
||||
Message: "subscriber channel updated successfully",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) ensureSubscriberExists(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
subscriberID := userID.String()
|
||||
|
||||
_, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
user, err := s.userProcessor.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get user data: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"userId": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"isActive": user.IsActive,
|
||||
"createdAt": user.CreatedAt,
|
||||
}
|
||||
|
||||
if user.Roles != nil && len(user.Roles) > 0 {
|
||||
roles := make([]map[string]interface{}, len(user.Roles))
|
||||
for i, role := range user.Roles {
|
||||
roles[i] = map[string]interface{}{
|
||||
"id": role.ID.String(),
|
||||
"name": role.Name,
|
||||
"code": role.Code,
|
||||
}
|
||||
}
|
||||
data["roles"] = roles
|
||||
}
|
||||
|
||||
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
|
||||
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
|
||||
for i, dept := range user.DepartmentResponse {
|
||||
depts[i] = map[string]interface{}{
|
||||
"id": dept.ID.String(),
|
||||
"name": dept.Name,
|
||||
"code": dept.Code,
|
||||
}
|
||||
}
|
||||
data["departments"] = depts
|
||||
}
|
||||
|
||||
subscriber := novu.SubscriberPayload{
|
||||
Email: user.Email,
|
||||
FirstName: user.Name,
|
||||
LastName: "",
|
||||
Phone: "",
|
||||
Avatar: "",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
_, err = s.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create subscriber: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,708 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"eslogad-be/config"
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/processor"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OnlyOfficeService interface {
|
||||
ProcessCallback(ctx context.Context, documentKey string, req *contract.OnlyOfficeCallbackRequest) (*contract.OnlyOfficeCallbackResponse, error)
|
||||
GetEditorConfig(ctx context.Context, req *contract.GetEditorConfigRequest) (*contract.GetEditorConfigResponse, error)
|
||||
LockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
||||
UnlockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
||||
GetDocumentSession(ctx context.Context, documentKey string) (*contract.DocumentSession, error)
|
||||
GetOnlyOfficeConfig(ctx context.Context) (*contract.OnlyOfficeConfigInfo, error)
|
||||
}
|
||||
|
||||
type OnlyOfficeServiceImpl struct {
|
||||
processor processor.OnlyOfficeProcessor
|
||||
documentBaseURL string
|
||||
callbackBaseURL string
|
||||
serverURL string
|
||||
jwtSecret string
|
||||
config *config.OnlyOffice
|
||||
db *gorm.DB
|
||||
fileStorage FileStorage
|
||||
docBucket string
|
||||
}
|
||||
|
||||
func NewOnlyOfficeService(processor processor.OnlyOfficeProcessor, cfg *config.OnlyOffice, db *gorm.DB, fileStorage FileStorage) *OnlyOfficeServiceImpl {
|
||||
return &OnlyOfficeServiceImpl{
|
||||
processor: processor,
|
||||
documentBaseURL: getEnvOrDefault("DOCUMENT_BASE_URL", "https://noken-log-api.tni-ad.mil.id/api/v1/files"),
|
||||
callbackBaseURL: getEnvOrDefault("CALLBACK_BASE_URL", "https://noken-log-api.tni-ad.mil.id/api/v1/onlyoffice/callback"),
|
||||
serverURL: cfg.URL,
|
||||
jwtSecret: cfg.Token,
|
||||
config: cfg,
|
||||
db: db,
|
||||
fileStorage: fileStorage,
|
||||
docBucket: "documents", // Use the same bucket as document uploads
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (s *OnlyOfficeServiceImpl) ProcessCallback(ctx context.Context, documentKey string, req *contract.OnlyOfficeCallbackRequest) (*contract.OnlyOfficeCallbackResponse, error) {
|
||||
// Verify JWT token if provided and secret is configured
|
||||
if req.Token != "" && s.jwtSecret != "" {
|
||||
claims, err := s.verifyJWT(req.Token)
|
||||
if err != nil {
|
||||
// Log the error but continue processing
|
||||
// OnlyOffice may not always send valid tokens
|
||||
fmt.Printf("JWT verification failed: %v\n", err)
|
||||
} else if claims != nil {
|
||||
// Extract data from JWT claims if needed
|
||||
if key, ok := claims["key"].(string); ok && key != documentKey {
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 1}, fmt.Errorf("document key mismatch in JWT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session, err := s.processor.GetDocumentSessionByKey(ctx, documentKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 1}, nil // Document key not found
|
||||
}
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 3}, err // Internal server error
|
||||
}
|
||||
|
||||
// Process based on status
|
||||
switch req.Status {
|
||||
case contract.OnlyOfficeStatusEditing:
|
||||
// Document is being edited
|
||||
err = s.handleEditingStatus(ctx, session, req)
|
||||
|
||||
case contract.OnlyOfficeStatusReady:
|
||||
// Document is ready for saving
|
||||
err = s.handleReadyStatus(ctx, session, req)
|
||||
|
||||
case contract.OnlyOfficeStatusSaveError:
|
||||
// Document saving error
|
||||
err = s.handleSaveError(ctx, session, req)
|
||||
|
||||
case contract.OnlyOfficeStatusClosed:
|
||||
// Document closed with no changes
|
||||
err = s.handleClosedStatus(ctx, session, req)
|
||||
|
||||
case contract.OnlyOfficeStatusForceSave:
|
||||
// Force save during editing
|
||||
err = s.handleForceSave(ctx, session, req)
|
||||
|
||||
case contract.OnlyOfficeStatusForceSaveError:
|
||||
// Force save error
|
||||
err = s.handleForceSaveError(ctx, session, req)
|
||||
|
||||
default:
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 3}, fmt.Errorf("unknown status: %d", req.Status)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 3}, err
|
||||
}
|
||||
|
||||
return &contract.OnlyOfficeCallbackResponse{Error: 0}, nil
|
||||
}
|
||||
|
||||
// handleEditingStatus handles when document is being edited
|
||||
func (s *OnlyOfficeServiceImpl) handleEditingStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
// Update session status
|
||||
session.Status = req.Status
|
||||
|
||||
// Lock document if not already locked
|
||||
if !session.IsLocked && len(req.Users) > 0 {
|
||||
userID := getOnlyOfficeUserIDFromContext(ctx)
|
||||
session.IsLocked = true
|
||||
session.LockedBy = &userID
|
||||
now := time.Now()
|
||||
session.LockedAt = &now
|
||||
}
|
||||
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
// handleReadyStatus handles when document is ready for saving
|
||||
func (s *OnlyOfficeServiceImpl) handleReadyStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
if req.URL == "" {
|
||||
return errors.New("document URL is required for saving")
|
||||
}
|
||||
|
||||
// Download the document
|
||||
documentData, err := s.downloadDocument(req.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download document: %w", err)
|
||||
}
|
||||
|
||||
// Use session UserID as SavedBy since callbacks don't have user context
|
||||
savedBy := session.UserID
|
||||
if savedBy == uuid.Nil {
|
||||
// Fallback to getting from context if available
|
||||
if userID := getOnlyOfficeUserIDFromContext(ctx); userID != uuid.Nil {
|
||||
savedBy = userID
|
||||
}
|
||||
}
|
||||
|
||||
// Save new version
|
||||
version := &entities.DocumentVersion{
|
||||
DocumentID: session.DocumentID,
|
||||
Version: session.Version + 1,
|
||||
FileSize: int64(len(documentData)),
|
||||
SavedBy: savedBy,
|
||||
SavedAt: time.Now(),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// For now, default to outgoing_attachment
|
||||
// In production, this should be stored in the session or document metadata
|
||||
documentType := "outgoing_attachment"
|
||||
|
||||
// Generate new file path and save
|
||||
fileName := fmt.Sprintf("v%d_%s_%s.docx", version.Version, time.Now().Format("20060102150405"), session.DocumentKey)
|
||||
filePath, err := s.saveDocumentFile(ctx, documentData, session.DocumentID, fileName, documentType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save document file: %w", err)
|
||||
}
|
||||
version.FileURL = filePath
|
||||
|
||||
// Save changes URL if provided
|
||||
if req.ChangesURL != "" {
|
||||
version.ChangesURL = &req.ChangesURL
|
||||
}
|
||||
|
||||
// Create new version
|
||||
err = s.processor.CreateDocumentVersion(ctx, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create document version: %w", err)
|
||||
}
|
||||
|
||||
// Update session
|
||||
session.Status = req.Status
|
||||
session.Version = version.Version
|
||||
now := time.Now()
|
||||
session.LastSavedAt = &now
|
||||
session.IsLocked = false
|
||||
session.LockedBy = nil
|
||||
session.LockedAt = nil
|
||||
|
||||
// Update the original document reference with new URL
|
||||
err = s.processor.UpdateDocumentURL(ctx, session.DocumentID, version.FileURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update document URL: %w", err)
|
||||
}
|
||||
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
// handleSaveError handles document save errors
|
||||
func (s *OnlyOfficeServiceImpl) handleSaveError(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
// Log the error
|
||||
s.processor.LogDocumentError(ctx, session.DocumentID, "Save error occurred", req)
|
||||
|
||||
// Update session status
|
||||
session.Status = req.Status
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
// handleClosedStatus handles when document is closed without changes
|
||||
func (s *OnlyOfficeServiceImpl) handleClosedStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
// Unlock document
|
||||
session.Status = req.Status
|
||||
session.IsLocked = false
|
||||
session.LockedBy = nil
|
||||
session.LockedAt = nil
|
||||
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
func (s *OnlyOfficeServiceImpl) handleForceSave(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
if req.URL == "" {
|
||||
return errors.New("document URL is required for force save")
|
||||
}
|
||||
|
||||
documentData, err := s.downloadDocument(req.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download document: %w", err)
|
||||
}
|
||||
|
||||
savedBy := session.UserID
|
||||
if savedBy == uuid.Nil {
|
||||
if userID := getOnlyOfficeUserIDFromContext(ctx); userID != uuid.Nil {
|
||||
savedBy = userID
|
||||
}
|
||||
}
|
||||
|
||||
version := &entities.DocumentVersion{
|
||||
DocumentID: session.DocumentID,
|
||||
Version: session.Version + 1,
|
||||
FileSize: int64(len(documentData)),
|
||||
SavedBy: savedBy,
|
||||
SavedAt: time.Now(),
|
||||
IsActive: false,
|
||||
Comments: stringPtr("Auto-save during editing"),
|
||||
}
|
||||
|
||||
// For now, default to outgoing_attachment
|
||||
// In production, this should be stored in the session or document metadata
|
||||
documentType := "outgoing_attachment"
|
||||
|
||||
fileName := fmt.Sprintf("autosave_v%d_%s_%s.docx", version.Version, time.Now().Format("20060102150405"), session.DocumentKey)
|
||||
filePath, err := s.saveDocumentFile(ctx, documentData, session.DocumentID, fileName, documentType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save document file: %w", err)
|
||||
}
|
||||
version.FileURL = filePath
|
||||
|
||||
err = s.processor.CreateDocumentVersion(ctx, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create document version: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
session.LastSavedAt = &now
|
||||
session.Version = version.Version
|
||||
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
// handleForceSaveError handles force save errors
|
||||
func (s *OnlyOfficeServiceImpl) handleForceSaveError(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
||||
// Log the error
|
||||
s.processor.LogDocumentError(ctx, session.DocumentID, "Force save error occurred", req)
|
||||
|
||||
// Update session status
|
||||
session.Status = req.Status
|
||||
return s.processor.UpdateDocumentSession(ctx, session)
|
||||
}
|
||||
|
||||
// downloadDocument downloads document from OnlyOffice
|
||||
func (s *OnlyOfficeServiceImpl) downloadDocument(url string) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to download document: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// saveDocumentFile saves document file to S3 storage
|
||||
func (s *OnlyOfficeServiceImpl) saveDocumentFile(ctx context.Context, data []byte, documentID uuid.UUID, fileName string, documentType string) (string, error) {
|
||||
// Ensure bucket exists
|
||||
if err := s.fileStorage.EnsureBucket(ctx, s.docBucket); err != nil {
|
||||
return "", fmt.Errorf("failed to ensure bucket: %w", err)
|
||||
}
|
||||
|
||||
// Create S3 key with date structure
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
key := fmt.Sprintf("onlyoffice/%s/%s/%s", documentID.String(), dateDir, fileName)
|
||||
|
||||
// Detect content type from file extension
|
||||
contentType := "application/octet-stream"
|
||||
ext := filepath.Ext(fileName)
|
||||
switch strings.ToLower(ext) {
|
||||
case ".docx":
|
||||
contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
case ".doc":
|
||||
contentType = "application/msword"
|
||||
case ".xlsx":
|
||||
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
case ".xls":
|
||||
contentType = "application/vnd.ms-excel"
|
||||
case ".pptx":
|
||||
contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
case ".ppt":
|
||||
contentType = "application/vnd.ms-powerpoint"
|
||||
case ".pdf":
|
||||
contentType = "application/pdf"
|
||||
}
|
||||
|
||||
// Upload to S3
|
||||
url, err := s.fileStorage.Upload(ctx, s.docBucket, key, data, contentType)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||
}
|
||||
|
||||
// Now update the attachment URL in the database
|
||||
if err := s.updateAttachmentURL(ctx, documentID, url, documentType); err != nil {
|
||||
// Log error but don't fail - the document is already saved
|
||||
fmt.Printf("Warning: Failed to update attachment URL: %v\n", err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// updateAttachmentURL updates the file_url in the appropriate attachment table
|
||||
func (s *OnlyOfficeServiceImpl) updateAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string, documentType string) error {
|
||||
switch documentType {
|
||||
case "letter_outgoing_attachment", "outgoing_attachment":
|
||||
return s.db.WithContext(ctx).
|
||||
Table("letter_outgoing_attachments").
|
||||
Where("id = ?", attachmentID).
|
||||
Update("file_url", newURL).Error
|
||||
|
||||
case "letter_incoming_attachment", "incoming_attachment":
|
||||
return s.db.WithContext(ctx).
|
||||
Table("letter_incoming_attachments").
|
||||
Where("id = ?", attachmentID).
|
||||
Update("file_url", newURL).Error
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported document type for URL update: %s", documentType)
|
||||
}
|
||||
}
|
||||
|
||||
// getDocumentFromAttachment retrieves document details directly from attachment tables
|
||||
func (s *OnlyOfficeServiceImpl) getDocumentFromAttachment(ctx context.Context, documentID uuid.UUID, documentType string) (*processor.DocumentDetails, error) {
|
||||
var fileName, fileURL, fileType string
|
||||
var fileSize int64
|
||||
|
||||
switch documentType {
|
||||
case "letter_outgoing_attachment", "outgoing_attachment":
|
||||
var attachment struct {
|
||||
FileName string `gorm:"column:file_name"`
|
||||
FileURL string `gorm:"column:file_url"`
|
||||
FileType string `gorm:"column:file_type"`
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).
|
||||
Table("letter_outgoing_attachments").
|
||||
Where("id = ?", documentID).
|
||||
Select("file_name, file_url, file_type").
|
||||
First(&attachment).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get outgoing attachment: %w", err)
|
||||
}
|
||||
|
||||
fileName = attachment.FileName
|
||||
fileURL = attachment.FileURL
|
||||
fileType = attachment.FileType
|
||||
|
||||
case "letter_incoming_attachment", "incoming_attachment":
|
||||
var attachment struct {
|
||||
FileName string `gorm:"column:file_name"`
|
||||
FileURL string `gorm:"column:file_url"`
|
||||
FileType string `gorm:"column:file_type"`
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).
|
||||
Table("letter_incoming_attachments").
|
||||
Where("id = ?", documentID).
|
||||
Select("file_name, file_url, file_type").
|
||||
First(&attachment).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get incoming attachment: %w", err)
|
||||
}
|
||||
|
||||
fileName = attachment.FileName
|
||||
fileURL = attachment.FileURL
|
||||
fileType = attachment.FileType
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported document type: %s", documentType)
|
||||
}
|
||||
|
||||
return &processor.DocumentDetails{
|
||||
DocumentID: documentID,
|
||||
FileName: fileName,
|
||||
FileType: fileType,
|
||||
FileURL: fileURL,
|
||||
FileSize: fileSize,
|
||||
DocumentType: documentType,
|
||||
ReferenceID: documentID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetEditorConfig generates OnlyOffice editor configuration
|
||||
func (s *OnlyOfficeServiceImpl) GetEditorConfig(ctx context.Context, req *contract.GetEditorConfigRequest) (*contract.GetEditorConfigResponse, error) {
|
||||
userCtx := appcontext.FromGinContext(ctx)
|
||||
if userCtx == nil {
|
||||
return nil, errors.New("user context not found")
|
||||
}
|
||||
|
||||
session, err := s.processor.GetOrCreateDocumentSession(ctx, req.DocumentID, userCtx.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get document session: %w", err)
|
||||
}
|
||||
|
||||
// Get document details directly from attachment tables
|
||||
document, err := s.getDocumentFromAttachment(ctx, req.DocumentID, req.DocumentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get document details: %w", err)
|
||||
}
|
||||
|
||||
documentKey := session.DocumentKey
|
||||
|
||||
fileExt := s.getFileExtension(document.FileName)
|
||||
if fileExt == "" || fileExt == strings.ToLower(document.FileName) {
|
||||
fileExt = s.getFileExtension(document.FileType)
|
||||
}
|
||||
|
||||
ooType := "desktop"
|
||||
if req.DocumentType == "incoming_attachment" {
|
||||
ooType = "embedded"
|
||||
}
|
||||
|
||||
config := &contract.OnlyOfficeConfigRequest{
|
||||
Document: &contract.OnlyOfficeDocument{
|
||||
FileType: fileExt,
|
||||
Key: documentKey,
|
||||
Title: document.FileName,
|
||||
URL: document.FileURL,
|
||||
Permissions: &contract.OnlyOfficePermissions{
|
||||
Comment: true,
|
||||
Download: true,
|
||||
Edit: req.Mode == "edit",
|
||||
FillForms: true,
|
||||
Print: true,
|
||||
Review: req.Mode == "edit",
|
||||
},
|
||||
Info: &contract.OnlyOfficeDocumentInfo{
|
||||
Owner: fmt.Sprintf("User-%s", userCtx.UserID.String()[:8]),
|
||||
Uploaded: time.Now().Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
},
|
||||
DocumentType: s.getDocumentType(fileExt), // Convert file extension to document type
|
||||
EditorConfig: &contract.OnlyOfficeEditorConfig{
|
||||
CallbackURL: fmt.Sprintf("%s/%s", s.callbackBaseURL, documentKey),
|
||||
Lang: "en",
|
||||
Mode: req.Mode,
|
||||
User: &contract.OnlyOfficeUserConfig{
|
||||
ID: userCtx.UserID.String(),
|
||||
Name: userCtx.UserName,
|
||||
},
|
||||
Customization: &contract.OnlyOfficeCustomization{
|
||||
Autosave: true,
|
||||
Comments: true,
|
||||
CompactHeader: false,
|
||||
ForceSave: true,
|
||||
Zoom: 100,
|
||||
},
|
||||
},
|
||||
Type: ooType, // Can be desktop, mobile, or embedded
|
||||
}
|
||||
|
||||
if s.jwtSecret != "" {
|
||||
token, err := s.generateJWT(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate JWT: %w", err)
|
||||
}
|
||||
config.Token = token
|
||||
}
|
||||
|
||||
return &contract.GetEditorConfigResponse{
|
||||
DocumentServerURL: s.serverURL,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LockDocument locks a document for editing
|
||||
func (s *OnlyOfficeServiceImpl) LockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error {
|
||||
return s.processor.LockDocument(ctx, documentID, userID)
|
||||
}
|
||||
|
||||
// UnlockDocument unlocks a document
|
||||
func (s *OnlyOfficeServiceImpl) UnlockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error {
|
||||
return s.processor.UnlockDocument(ctx, documentID, userID)
|
||||
}
|
||||
|
||||
// GetDocumentSession gets document session by key
|
||||
func (s *OnlyOfficeServiceImpl) GetDocumentSession(ctx context.Context, documentKey string) (*contract.DocumentSession, error) {
|
||||
session, err := s.processor.GetDocumentSessionByKey(ctx, documentKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.DocumentSession{
|
||||
ID: session.ID,
|
||||
DocumentID: session.DocumentID,
|
||||
DocumentKey: session.DocumentKey,
|
||||
UserID: session.UserID,
|
||||
Status: session.Status,
|
||||
IsLocked: session.IsLocked,
|
||||
LockedBy: session.LockedBy,
|
||||
LockedAt: session.LockedAt,
|
||||
LastSavedAt: session.LastSavedAt,
|
||||
Version: session.Version,
|
||||
CreatedAt: session.CreatedAt,
|
||||
UpdatedAt: session.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateDocumentKey generates a unique key for OnlyOffice
|
||||
func (s *OnlyOfficeServiceImpl) generateDocumentKey(documentID uuid.UUID, version int) string {
|
||||
// Use nanoseconds and random bytes for uniqueness
|
||||
randomBytes := make([]byte, 8)
|
||||
rand.Read(randomBytes)
|
||||
data := fmt.Sprintf("%s_%d_%d_%s", documentID.String(), version, time.Now().UnixNano(), hex.EncodeToString(randomBytes))
|
||||
hash := md5.Sum([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// getFileExtension extracts the file extension from file type or filename
|
||||
func (s *OnlyOfficeServiceImpl) getFileExtension(fileType string) string {
|
||||
// Remove any leading dot
|
||||
fileType = strings.TrimPrefix(fileType, ".")
|
||||
|
||||
// If fileType contains a dot, extract the extension after the last dot
|
||||
if strings.Contains(fileType, ".") {
|
||||
parts := strings.Split(fileType, ".")
|
||||
if len(parts) > 1 {
|
||||
return strings.ToLower(parts[len(parts)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, return as is (assuming it's already an extension like "docx", "xlsx", etc.)
|
||||
return strings.ToLower(fileType)
|
||||
}
|
||||
|
||||
// getDocumentType determines OnlyOffice document type from file extension
|
||||
func (s *OnlyOfficeServiceImpl) getDocumentType(fileType string) string {
|
||||
fileType = strings.ToLower(fileType)
|
||||
|
||||
// Remove dot if present
|
||||
fileType = strings.TrimPrefix(fileType, ".")
|
||||
|
||||
// Text documents
|
||||
if fileType == "doc" || fileType == "docx" || fileType == "docm" ||
|
||||
fileType == "dot" || fileType == "dotx" || fileType == "dotm" ||
|
||||
fileType == "odt" || fileType == "fodt" || fileType == "ott" ||
|
||||
fileType == "rtf" || fileType == "txt" || fileType == "html" ||
|
||||
fileType == "htm" || fileType == "mht" || fileType == "pdf" ||
|
||||
fileType == "djvu" || fileType == "fb2" || fileType == "epub" ||
|
||||
fileType == "xps" {
|
||||
return "word"
|
||||
}
|
||||
|
||||
// Spreadsheets
|
||||
if fileType == "xls" || fileType == "xlsx" || fileType == "xlsm" ||
|
||||
fileType == "xlt" || fileType == "xltx" || fileType == "xltm" ||
|
||||
fileType == "ods" || fileType == "fods" || fileType == "ots" ||
|
||||
fileType == "csv" {
|
||||
return "cell"
|
||||
}
|
||||
|
||||
// Presentations
|
||||
if fileType == "pps" || fileType == "ppsx" || fileType == "ppsm" ||
|
||||
fileType == "ppt" || fileType == "pptx" || fileType == "pptm" ||
|
||||
fileType == "pot" || fileType == "potx" || fileType == "potm" ||
|
||||
fileType == "odp" || fileType == "fodp" || fileType == "otp" {
|
||||
return "presentation"
|
||||
}
|
||||
|
||||
// Default to text
|
||||
return "slide"
|
||||
}
|
||||
|
||||
// generateJWT generates JWT token for OnlyOffice
|
||||
func (s *OnlyOfficeServiceImpl) generateJWT(config *contract.OnlyOfficeConfigRequest) (string, error) {
|
||||
// OnlyOffice expects the entire config to be in the JWT payload
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
// Convert the config struct to a map for JWT payload
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal(configJSON, &configMap); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal config to map: %w", err)
|
||||
}
|
||||
|
||||
// Add all config fields to the payload
|
||||
for key, value := range configMap {
|
||||
payload[key] = value
|
||||
}
|
||||
|
||||
// Create the token with the payload
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
|
||||
|
||||
// Sign the token with the secret
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func getOnlyOfficeUserIDFromContext(ctx context.Context) uuid.UUID {
|
||||
userCtx := appcontext.FromGinContext(ctx)
|
||||
if userCtx != nil {
|
||||
return userCtx.UserID
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// GetOnlyOfficeConfig returns the OnlyOffice configuration
|
||||
func (s *OnlyOfficeServiceImpl) GetOnlyOfficeConfig(ctx context.Context) (*contract.OnlyOfficeConfigInfo, error) {
|
||||
return &contract.OnlyOfficeConfigInfo{
|
||||
URL: s.config.URL,
|
||||
Token: s.config.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// verifyJWT verifies JWT token from OnlyOffice
|
||||
func (s *OnlyOfficeServiceImpl) verifyJWT(tokenString string) (jwt.MapClaims, error) {
|
||||
if s.jwtSecret == "" {
|
||||
// If no secret is configured, skip verification
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid JWT token")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to parse JWT claims")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/repository"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RBACServiceImpl struct {
|
||||
repo *repository.RBACRepository
|
||||
}
|
||||
|
||||
func NewRBACService(repo *repository.RBACRepository) *RBACServiceImpl {
|
||||
return &RBACServiceImpl{repo: repo}
|
||||
}
|
||||
|
||||
// Permissions
|
||||
func (s *RBACServiceImpl) CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error) {
|
||||
p := &entities.Permission{Code: req.Code}
|
||||
if req.Description != nil {
|
||||
p.Description = *req.Description
|
||||
}
|
||||
if err := s.repo.CreatePermission(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.PermissionResponse{
|
||||
ID: p.ID,
|
||||
Code: p.Code,
|
||||
Action: p.Action,
|
||||
Description: &p.Description,
|
||||
}, nil
|
||||
}
|
||||
func (s *RBACServiceImpl) UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error) {
|
||||
p := &entities.Permission{ID: id}
|
||||
if req.Code != nil {
|
||||
p.Code = *req.Code
|
||||
}
|
||||
if req.Description != nil {
|
||||
p.Description = *req.Description
|
||||
}
|
||||
if err := s.repo.UpdatePermission(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// fetch full row
|
||||
perms, err := s.repo.ListPermissions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, x := range perms {
|
||||
if x.ID == id {
|
||||
return &contract.PermissionResponse{
|
||||
ID: x.ID,
|
||||
Code: x.Code,
|
||||
Action: x.Action,
|
||||
Description: &x.Description,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (s *RBACServiceImpl) DeletePermission(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.DeletePermission(ctx, id)
|
||||
}
|
||||
func (s *RBACServiceImpl) ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error) {
|
||||
perms, err := s.repo.ListPermissions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListPermissionsResponse{Permissions: transformer.PermissionsToContract(perms)}, nil
|
||||
}
|
||||
|
||||
// Roles
|
||||
func (s *RBACServiceImpl) CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
|
||||
role := &entities.Role{Name: req.Name, Code: req.Code}
|
||||
if req.Description != nil {
|
||||
role.Description = *req.Description
|
||||
}
|
||||
if err := s.repo.CreateRole(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(req.PermissionCodes) > 0 {
|
||||
_ = s.repo.SetRolePermissionsByCodes(ctx, role.ID, req.PermissionCodes)
|
||||
}
|
||||
perms, _ := s.repo.GetPermissionsByRoleID(ctx, role.ID)
|
||||
resp := transformer.RoleWithPermissionsToContract(*role, perms)
|
||||
return &resp, nil
|
||||
}
|
||||
func (s *RBACServiceImpl) UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
|
||||
role := &entities.Role{ID: id}
|
||||
if req.Name != nil {
|
||||
role.Name = *req.Name
|
||||
}
|
||||
if req.Code != nil {
|
||||
role.Code = *req.Code
|
||||
}
|
||||
if req.Description != nil {
|
||||
role.Description = *req.Description
|
||||
}
|
||||
if err := s.repo.UpdateRole(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.PermissionCodes != nil {
|
||||
_ = s.repo.SetRolePermissionsByCodes(ctx, id, *req.PermissionCodes)
|
||||
}
|
||||
perms, _ := s.repo.GetPermissionsByRoleID(ctx, id)
|
||||
// fetch updated role
|
||||
roles, err := s.repo.ListRoles(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range roles {
|
||||
if r.ID == id {
|
||||
resp := transformer.RoleWithPermissionsToContract(r, perms)
|
||||
return &resp, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (s *RBACServiceImpl) DeleteRole(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.DeleteRole(ctx, id)
|
||||
}
|
||||
func (s *RBACServiceImpl) ListRoles(ctx context.Context) (*contract.ListRolesResponse, error) {
|
||||
roles, err := s.repo.ListRoles(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]contract.RoleWithPermissionsResponse, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
perms, _ := s.repo.GetPermissionsByRoleID(ctx, r.ID)
|
||||
out = append(out, transformer.RoleWithPermissionsToContract(r, perms))
|
||||
}
|
||||
return &contract.ListRolesResponse{Roles: out}, nil
|
||||
}
|
||||
|
||||
// New methods for the required API endpoints
|
||||
func (s *RBACServiceImpl) GetPermissionsGrouped(ctx context.Context) (*contract.PermissionsGroupedResponse, error) {
|
||||
modules, err := s.repo.ListModules(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]contract.ModuleWithPermissionsResponse, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
perms, err := s.repo.ListPermissions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modulePerms := make([]contract.PermissionResponse, 0)
|
||||
for _, perm := range perms {
|
||||
if perm.ModuleID != nil && *perm.ModuleID == module.ID {
|
||||
modulePerms = append(modulePerms, contract.PermissionResponse{
|
||||
ID: perm.ID,
|
||||
Code: perm.Code,
|
||||
Action: perm.Action,
|
||||
Description: &perm.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, contract.ModuleWithPermissionsResponse{
|
||||
Module: contract.ModuleResponse{
|
||||
ID: module.ID,
|
||||
Name: module.Name,
|
||||
Code: module.Code,
|
||||
},
|
||||
Permissions: modulePerms,
|
||||
})
|
||||
}
|
||||
|
||||
return &contract.PermissionsGroupedResponse{Data: result}, nil
|
||||
}
|
||||
|
||||
func (s *RBACServiceImpl) CreateOrUpdateRole(ctx context.Context, req *contract.CreateOrUpdateRoleRequest) (*contract.RoleDetailResponse, error) {
|
||||
// Check if role exists
|
||||
existingRole, _ := s.repo.GetRoleByCode(ctx, req.Code)
|
||||
|
||||
var role *entities.Role
|
||||
if existingRole != nil {
|
||||
// Update existing role
|
||||
role = existingRole
|
||||
role.Name = req.Name
|
||||
role.Description = req.Description
|
||||
if err := s.repo.UpdateRole(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Create new role
|
||||
role = &entities.Role{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := s.repo.CreateRole(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Set permissions based on module and actions
|
||||
permissionIDs := make([]uuid.UUID, 0)
|
||||
for _, modPerm := range req.Permissions {
|
||||
_, err := s.repo.GetModuleByCode(ctx, modPerm.Module)
|
||||
if err != nil {
|
||||
continue // Skip if module not found
|
||||
}
|
||||
|
||||
for _, action := range modPerm.Actions {
|
||||
permCode := modPerm.Module + "_" + action
|
||||
perm, err := s.repo.GetPermissionByCode(ctx, permCode)
|
||||
if err == nil && perm != nil {
|
||||
permissionIDs = append(permissionIDs, perm.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.repo.SetRolePermissionsByIDs(ctx, role.ID, permissionIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build response
|
||||
return s.GetRoleDetail(ctx, role.ID)
|
||||
}
|
||||
|
||||
func (s *RBACServiceImpl) GetRoleDetail(ctx context.Context, roleID uuid.UUID) (*contract.RoleDetailResponse, error) {
|
||||
role, err := s.repo.GetRoleByID(ctx, roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissions, err := s.repo.GetPermissionsByRoleID(ctx, roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Group permissions by module
|
||||
moduleMap := make(map[uuid.UUID]*contract.RolePermissionModuleResponse)
|
||||
|
||||
for _, perm := range permissions {
|
||||
if perm.ModuleID == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := moduleMap[*perm.ModuleID]; !exists {
|
||||
if perm.Module != nil {
|
||||
moduleMap[*perm.ModuleID] = &contract.RolePermissionModuleResponse{
|
||||
Module: contract.ModuleResponse{
|
||||
ID: perm.Module.ID,
|
||||
Name: perm.Module.Name,
|
||||
Code: perm.Module.Code,
|
||||
},
|
||||
Actions: []contract.PermissionActionResponse{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if modResp, exists := moduleMap[*perm.ModuleID]; exists {
|
||||
modResp.Actions = append(modResp.Actions, contract.PermissionActionResponse{
|
||||
ID: perm.ID,
|
||||
Action: perm.Action,
|
||||
Code: perm.Code,
|
||||
Description: perm.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice
|
||||
permissionModules := make([]contract.RolePermissionModuleResponse, 0, len(moduleMap))
|
||||
for _, modResp := range moduleMap {
|
||||
permissionModules = append(permissionModules, *modResp)
|
||||
}
|
||||
|
||||
return &contract.RoleDetailResponse{
|
||||
ID: role.ID,
|
||||
Name: role.Name,
|
||||
Code: role.Code,
|
||||
Description: role.Description,
|
||||
CreatedAt: role.CreatedAt,
|
||||
UpdatedAt: role.UpdatedAt,
|
||||
Permissions: permissionModules,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RepositoryAttachmentProcessor interface {
|
||||
CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error)
|
||||
DeleteAttachment(ctx context.Context, id uuid.UUID) error
|
||||
GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error)
|
||||
ListAttachment(ctx context.Context, search *string, limit, offset int) ([]contract.RepositoryAttachmentsResponse, int, error)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RepositoryAttachmentServiceImpl struct {
|
||||
attachmentProcessor RepositoryAttachmentProcessor
|
||||
}
|
||||
|
||||
func NewRepositoryAttachmentService(attachmentProcessor RepositoryAttachmentProcessor) *RepositoryAttachmentServiceImpl {
|
||||
return &RepositoryAttachmentServiceImpl{
|
||||
attachmentProcessor: attachmentProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RepositoryAttachmentServiceImpl) CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) {
|
||||
return s.attachmentProcessor.CreateAttachment(ctx, req)
|
||||
}
|
||||
|
||||
func (s *RepositoryAttachmentServiceImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error {
|
||||
return s.attachmentProcessor.DeleteAttachment(ctx, id)
|
||||
}
|
||||
|
||||
func (s *RepositoryAttachmentServiceImpl) GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) {
|
||||
return s.attachmentProcessor.GetById(ctx, id)
|
||||
}
|
||||
|
||||
func (s *RepositoryAttachmentServiceImpl) ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error) {
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit to prevent performance issues
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Pass calculated offset and limit to processor
|
||||
attachmentResponses, totalCount, err := s.attachmentProcessor.ListAttachment(ctx, req.Search, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.ListRepositoryAttachmentsResponse{
|
||||
Attachments: attachmentResponses,
|
||||
Pagination: transformer.CreatePaginationResponse(totalCount, page, limit),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserProcessor interface {
|
||||
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
|
||||
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
||||
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
||||
ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error
|
||||
|
||||
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
|
||||
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)
|
||||
GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error)
|
||||
|
||||
GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
|
||||
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||
|
||||
// New optimized listing
|
||||
ListUsersWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]contract.UserResponse, int, error)
|
||||
|
||||
// Get active users for mention purposes
|
||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||
}
|
||||
@@ -2,122 +2,92 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/transformer"
|
||||
"go-backend-template/internal/contract"
|
||||
"go-backend-template/internal/repository"
|
||||
"go-backend-template/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserServiceImpl struct {
|
||||
userProcessor UserProcessor
|
||||
titleRepo TitleRepository
|
||||
userRepo *repository.UserRepositoryImpl
|
||||
}
|
||||
|
||||
type TitleRepository interface {
|
||||
ListAll(ctx context.Context) ([]entities.Title, error)
|
||||
}
|
||||
|
||||
func NewUserService(userProcessor UserProcessor, titleRepo TitleRepository) *UserServiceImpl {
|
||||
func NewUserService(userRepo *repository.UserRepositoryImpl) *UserServiceImpl {
|
||||
return &UserServiceImpl{
|
||||
userProcessor: userProcessor,
|
||||
titleRepo: titleRepo,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) {
|
||||
return s.userProcessor.CreateUser(ctx, req)
|
||||
}
|
||||
// Check if user already exists
|
||||
existingUser, _ := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if existingUser != nil {
|
||||
return nil, errors.New("user with this email already exists")
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
|
||||
return s.userProcessor.UpdateUser(ctx, id, req)
|
||||
}
|
||||
// Hash password
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
return s.userProcessor.DeleteUser(ctx, id)
|
||||
// Create user entity
|
||||
user := transformer.CreateUserRequestToEntity(req, string(passwordHash))
|
||||
user.ID = uuid.New()
|
||||
|
||||
// Save to database
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
|
||||
return s.userProcessor.GetUserByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
||||
return s.userProcessor.GetUserByEmail(ctx, email)
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) {
|
||||
// Handle pagination parameters in service layer
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit to prevent performance issues
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Pass calculated offset and limit to processor
|
||||
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.ListUsersResponse{
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) GetUsers(ctx context.Context, page, limit int) (*contract.PaginatedUserResponse, error) {
|
||||
users, totalCount, err := s.userRepo.GetAll(ctx, page, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userResponses := transformer.EntitiesToContracts(users)
|
||||
pagination := transformer.CreatePaginationResponse(int(totalCount), page, limit)
|
||||
|
||||
return &contract.PaginatedUserResponse{
|
||||
Users: userResponses,
|
||||
Pagination: transformer.CreatePaginationResponse(totalCount, page, limit),
|
||||
Pagination: pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error {
|
||||
return s.userProcessor.ChangePassword(ctx, userID, req)
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error {
|
||||
return s.userProcessor.ChangeUserPassword(ctx, userID, req)
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
||||
prof, err := s.userProcessor.GetUserProfile(ctx, userID)
|
||||
func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if roles, err := s.userProcessor.GetUserRoles(ctx, userID); err == nil {
|
||||
prof.Roles = roles
|
||||
}
|
||||
return prof, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {
|
||||
return s.userProcessor.UpdateUserProfile(ctx, userID, req)
|
||||
}
|
||||
// Update user fields
|
||||
updatedUser := transformer.UpdateUserEntity(user, req)
|
||||
|
||||
func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesResponse, error) {
|
||||
if s.titleRepo == nil {
|
||||
return &contract.ListTitlesResponse{Titles: []contract.TitleResponse{}}, nil
|
||||
}
|
||||
titles, err := s.titleRepo.ListAll(ctx)
|
||||
if err != nil {
|
||||
// Save to database
|
||||
if err := s.userRepo.Update(ctx, updatedUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contract.ListTitlesResponse{Titles: transformer.TitlesToContract(titles)}, nil
|
||||
|
||||
return transformer.EntityToContract(updatedUser), nil
|
||||
}
|
||||
|
||||
// GetActiveUsersForMention retrieves active users for mention purposes
|
||||
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||
// Handle limit in service layer
|
||||
if limit <= 0 {
|
||||
limit = 50 // Default limit for mention suggestions
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit to prevent performance issues
|
||||
}
|
||||
|
||||
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
|
||||
func (s *UserServiceImpl) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
return s.userRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user