package service import ( "database/sql" "errors" "fmt" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository" "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database" "go.uber.org/zap" ) var ( ErrEmailExists = errors.New("email already exists") ErrUserNotCreated = errors.New("failed to create user") ErrInvalidRole = errors.New("invalid user role") ErrInvalidPatient = errors.New("invalid patient data") ErrInvalidDoctor = errors.New("invalid doctor data") ErrTransaction = errors.New("transaction error") ) // RegisterRequest represents a user registration request type RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` Role string `json:"role"` // "patient", "ref_doctor", "expertise_doctor", or "admin" Patient *models.PatientDetails `json:"patient,omitempty"` Doctor *models.DoctorDetails `json:"doctor,omitempty"` Studies []models.Study `json:"studies,omitempty"` // Study records for patient } // RegisterService handles user registration type RegisterService struct { userRepo *repository.UserRepository patientRepo *repository.PatientRepository doctorRepo *repository.DoctorRepository studyRepo *repository.StudyRepository logger *zap.Logger } // NewRegisterService creates a new register service func NewRegisterService(logger *zap.Logger) *RegisterService { if logger == nil { // If no logger is provided, create a no-op logger logger = zap.NewNop() } return &RegisterService{ userRepo: repository.NewUserRepository(), patientRepo: repository.NewPatientRepository(), doctorRepo: repository.NewDoctorRepository(), studyRepo: repository.NewStudyRepository(), logger: logger, } } // Register creates a new user with their associated role-specific data func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) { // Validate role if req.Role != "patient" && req.Role != "ref_doctor" && req.Role != "expertise_doctor" && req.Role != "admin" { return nil, ErrInvalidRole } // Check role-specific data if req.Role == "patient" && (req.Patient == nil || req.Patient.PatientID == "" || req.Patient.DateOfBirth == "") { return nil, ErrInvalidPatient } if (req.Role == "ref_doctor" || req.Role == "expertise_doctor") && (req.Doctor == nil || req.Doctor.DoctorID == "") { return nil, ErrInvalidDoctor } // Check if email already exists - do this outside the transaction existingUser, err := s.userRepo.GetUserByEmail(req.Email) if err != nil && err != sql.ErrNoRows { return nil, fmt.Errorf("error checking existing user: %w", err) } if existingUser != nil { return nil, ErrEmailExists } // Start a transaction tx, err := database.DB.Beginx() if err != nil { s.logger.Error("Failed to begin transaction", zap.Error(err)) return nil, fmt.Errorf("%w: failed to begin transaction", ErrTransaction) } // Ensure the transaction is rolled back if we return an error defer func() { if r := recover(); r != nil { s.logger.Error("Panic in transaction", zap.Any("recover", r)) tx.Rollback() panic(r) // re-throw the panic after cleanup } }() // Create user newUser := &models.User{ Email: req.Email, Password: req.Password, // Will be hashed in repository Role: req.Role, Name: req.Name, } // Use the transaction for user creation if err := s.userRepo.CreateUserTx(tx, newUser); err != nil { s.logger.Error("Failed to create user", zap.Error(err), zap.String("email", req.Email)) tx.Rollback() return nil, fmt.Errorf("error creating user: %w", err) } // Create role-specific data if req.Role == "patient" { err = s.patientRepo.CreatePatientTx(tx, req.Patient, newUser.ID) if err != nil { s.logger.Error("Failed to create patient", zap.Error(err), zap.String("patientID", req.Patient.PatientID)) tx.Rollback() return nil, fmt.Errorf("error creating patient: %w", err) } // Create associated study records if provided if len(req.Studies) > 0 { for _, study := range req.Studies { if study.StudyInstanceUID == "" { continue // Skip studies without UIDs } err = s.studyRepo.CreateStudyTx(tx, req.Patient.PatientID, study) if err != nil { s.logger.Error("Failed to create study", zap.Error(err), zap.String("patientID", req.Patient.PatientID), zap.String("studyUID", study.StudyInstanceUID)) tx.Rollback() return nil, fmt.Errorf("error creating study for patient: %w", err) } } } } else if req.Role == "ref_doctor" || req.Role == "expertise_doctor" { err = s.doctorRepo.CreateDoctorTx(tx, req.Doctor, newUser.ID) if err != nil { s.logger.Error("Failed to create doctor", zap.Error(err), zap.String("doctorID", req.Doctor.DoctorID)) tx.Rollback() return nil, fmt.Errorf("error creating doctor: %w", err) } } // Commit the transaction if err := tx.Commit(); err != nil { s.logger.Error("Failed to commit transaction", zap.Error(err)) tx.Rollback() // This is actually redundant as the transaction will be rolled back on failure return nil, fmt.Errorf("%w: failed to commit transaction", ErrTransaction) } s.logger.Info("Successfully registered new user", zap.String("email", req.Email), zap.String("role", req.Role), zap.String("userID", newUser.ID)) return newUser, nil }