package profissionais import ( "context" "errors" "fmt" "photum-backend/internal/db/generated" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) type Service struct { queries *generated.Queries } func NewService(queries *generated.Queries) *Service { return &Service{queries: queries} } type CreateProfissionalInput struct { Nome string `json:"nome"` FuncaoProfissionalID string `json:"funcao_profissional_id"` FuncoesIds []string `json:"funcoes_ids"` // New field Endereco *string `json:"endereco"` Cidade *string `json:"cidade"` Uf *string `json:"uf"` Whatsapp *string `json:"whatsapp"` CpfCnpjTitular *string `json:"cpf_cnpj_titular"` Banco *string `json:"banco"` Agencia *string `json:"agencia"` Conta *string `json:"conta"` ContaPix *string `json:"conta_pix"` CarroDisponivel *bool `json:"carro_disponivel"` TemEstudio *bool `json:"tem_estudio"` QtdEstudio *int `json:"qtd_estudio"` TipoCartao *string `json:"tipo_cartao"` Observacao *string `json:"observacao"` QualTec *int `json:"qual_tec"` EducacaoSimpatia *int `json:"educacao_simpatia"` DesempenhoEvento *int `json:"desempenho_evento"` DispHorario *int `json:"disp_horario"` Media *float64 `json:"media"` TabelaFree *string `json:"tabela_free"` ExtraPorEquipamento *bool `json:"extra_por_equipamento"` Equipamentos *string `json:"equipamentos"` Email *string `json:"email"` AvatarURL *string `json:"avatar_url"` TargetUserID *string `json:"target_user_id"` // Optional: For admin creation Regiao *string `json:"regiao"` // Optional: Override region } func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput, regiao string) (*generated.GetProfissionalByIDRow, error) { finalUserID := userID if input.TargetUserID != nil && *input.TargetUserID != "" { finalUserID = *input.TargetUserID } usuarioUUID, err := uuid.Parse(finalUserID) if err != nil { return nil, errors.New("invalid usuario_id") } // Check if CPF/CNPJ is provided and if it exists (Claim flow) if input.CpfCnpjTitular != nil && *input.CpfCnpjTitular != "" { existing, err := s.queries.GetProfissionalByCPF(ctx, generated.GetProfissionalByCPFParams{ CpfCnpjTitular: pgtype.Text{String: *input.CpfCnpjTitular, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err == nil { // Found! Check if already claimed if existing.UsuarioID.Valid { return nil, errors.New("professional with this CPF already has a user associated") } // Not claimed -> Link and Update // Helper for merging (prefer new input, fallback to existing) mergeStr := func(newVal *string, oldVal pgtype.Text) *string { if newVal != nil { return newVal } if oldVal.Valid { s := oldVal.String return &s } return nil } mergeInt := func(newVal *int, oldVal pgtype.Int4) *int { if newVal != nil { return newVal } if oldVal.Valid { i := int(oldVal.Int32) return &i } return nil } mergeBool := func(newVal *bool, oldVal pgtype.Bool) *bool { if newVal != nil { return newVal } if oldVal.Valid { b := oldVal.Bool return &b } return nil } mergeFloat := func(newVal *float64, oldVal pgtype.Numeric) *float64 { if newVal != nil { return newVal } if oldVal.Valid { f, _ := oldVal.Float64Value() v := f.Float64 return &v } return nil } // Use Update Logic with Merging updateInput := UpdateProfissionalInput{ Nome: input.Nome, FuncaoProfissionalID: input.FuncaoProfissionalID, FuncoesIds: input.FuncoesIds, Endereco: mergeStr(input.Endereco, existing.Endereco), Cidade: mergeStr(input.Cidade, existing.Cidade), Uf: mergeStr(input.Uf, existing.Uf), Whatsapp: mergeStr(input.Whatsapp, existing.Whatsapp), CpfCnpjTitular: mergeStr(input.CpfCnpjTitular, existing.CpfCnpjTitular), Banco: mergeStr(input.Banco, existing.Banco), Agencia: mergeStr(input.Agencia, existing.Agencia), Conta: mergeStr(input.Conta, existing.Conta), ContaPix: mergeStr(input.ContaPix, existing.ContaPix), CarroDisponivel: mergeBool(input.CarroDisponivel, existing.CarroDisponivel), TemEstudio: mergeBool(input.TemEstudio, existing.TemEstudio), QtdEstudio: mergeInt(input.QtdEstudio, existing.QtdEstudio), TipoCartao: mergeStr(input.TipoCartao, existing.TipoCartao), Observacao: mergeStr(input.Observacao, existing.Observacao), QualTec: mergeInt(input.QualTec, existing.QualTec), EducacaoSimpatia: mergeInt(input.EducacaoSimpatia, existing.EducacaoSimpatia), DesempenhoEvento: mergeInt(input.DesempenhoEvento, existing.DesempenhoEvento), DispHorario: mergeInt(input.DispHorario, existing.DispHorario), Media: mergeFloat(input.Media, existing.Media), TabelaFree: mergeStr(input.TabelaFree, existing.TabelaFree), ExtraPorEquipamento: mergeBool(input.ExtraPorEquipamento, existing.ExtraPorEquipamento), Equipamentos: mergeStr(input.Equipamentos, existing.Equipamentos), Email: mergeStr(input.Email, existing.Email), AvatarURL: mergeStr(input.AvatarURL, existing.AvatarUrl), } // Link User to Professional idStr := uuid.UUID(existing.ID.Bytes).String() err = s.queries.LinkUserToProfessional(ctx, generated.LinkUserToProfessionalParams{ ID: existing.ID, UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true}, }) if err != nil { return nil, err } // Update other fields _, err = s.Update(ctx, idStr, updateInput, regiao) if err != nil { // Should rollback link? Ideally within transaction. // For now, return error. return nil, err } // Return updated professional (fetch again to be sure or use return from Update) return s.GetByID(ctx, idStr, regiao) } } var funcaoUUID uuid.UUID var funcaoValid bool if input.FuncaoProfissionalID != "" { parsed, err := uuid.Parse(input.FuncaoProfissionalID) if err != nil { return nil, errors.New("invalid funcao_profissional_id") } if parsed == uuid.Nil { funcaoValid = false } else { funcaoUUID = parsed funcaoValid = true } } else { funcaoValid = false } params := generated.CreateProfissionalParams{ UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true}, Nome: input.Nome, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid}, Endereco: toPgText(input.Endereco), Cidade: toPgText(input.Cidade), Uf: toPgText(input.Uf), Whatsapp: toPgText(input.Whatsapp), CpfCnpjTitular: toPgText(input.CpfCnpjTitular), Banco: toPgText(input.Banco), Agencia: toPgText(input.Agencia), Conta: toPgText(input.Conta), ContaPix: toPgText(input.ContaPix), CarroDisponivel: toPgBool(input.CarroDisponivel), TemEstudio: toPgBool(input.TemEstudio), QtdEstudio: toPgInt4(input.QtdEstudio), TipoCartao: toPgText(input.TipoCartao), Observacao: toPgText(input.Observacao), QualTec: toPgInt4(input.QualTec), EducacaoSimpatia: toPgInt4(input.EducacaoSimpatia), DesempenhoEvento: toPgInt4(input.DesempenhoEvento), DispHorario: toPgInt4(input.DispHorario), Media: toPgNumeric(input.Media), TabelaFree: toPgText(input.TabelaFree), ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento), Equipamentos: toPgText(input.Equipamentos), Email: toPgText(input.Email), AvatarUrl: toPgText(input.AvatarURL), Regiao: pgtype.Text{String: regiao, Valid: true}, } prof, err := s.queries.CreateProfissional(ctx, params) if err != nil { return nil, err } // Insert multiple functions if provided if len(input.FuncoesIds) > 0 { for _, fid := range input.FuncoesIds { fUUID, err := uuid.Parse(fid) if err == nil { _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true}, }) } } } else if funcaoValid { // If no list provided but single ID is, insert that one too into junction _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, FuncaoID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, }) } // Fetch full object to return return s.GetByID(ctx, uuid.UUID(prof.ID.Bytes).String(), regiao) } func (s *Service) List(ctx context.Context, regiao string) ([]generated.ListProfissionaisRow, error) { return s.queries.ListProfissionais(ctx, pgtype.Text{String: regiao, Valid: true}) } func (s *Service) GetByID(ctx context.Context, id string, regiao string) (*generated.GetProfissionalByIDRow, error) { uuidVal, err := uuid.Parse(id) if err != nil { return nil, errors.New("invalid id") } prof, err := s.queries.GetProfissionalByID(ctx, generated.GetProfissionalByIDParams{ ID: pgtype.UUID{Bytes: uuidVal, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err != nil { return nil, err } return &prof, nil } type UpdateProfissionalInput struct { Nome string `json:"nome"` FuncaoProfissionalID string `json:"funcao_profissional_id"` FuncoesIds []string `json:"funcoes_ids"` // New field Endereco *string `json:"endereco"` Cidade *string `json:"cidade"` Uf *string `json:"uf"` Whatsapp *string `json:"whatsapp"` CpfCnpjTitular *string `json:"cpf_cnpj_titular"` Banco *string `json:"banco"` Agencia *string `json:"agencia"` Conta *string `json:"conta"` ContaPix *string `json:"conta_pix"` CarroDisponivel *bool `json:"carro_disponivel"` TemEstudio *bool `json:"tem_estudio"` QtdEstudio *int `json:"qtd_estudio"` TipoCartao *string `json:"tipo_cartao"` Observacao *string `json:"observacao"` QualTec *int `json:"qual_tec"` EducacaoSimpatia *int `json:"educacao_simpatia"` DesempenhoEvento *int `json:"desempenho_evento"` DispHorario *int `json:"disp_horario"` Media *float64 `json:"media"` TabelaFree *string `json:"tabela_free"` ExtraPorEquipamento *bool `json:"extra_por_equipamento"` Equipamentos *string `json:"equipamentos"` Email *string `json:"email"` AvatarURL *string `json:"avatar_url"` } func (s *Service) Update(ctx context.Context, id string, input UpdateProfissionalInput, regiao string) (*generated.CadastroProfissionai, error) { uuidVal, err := uuid.Parse(id) if err != nil { return nil, errors.New("invalid id") } funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID) if err != nil { return nil, errors.New("invalid funcao_profissional_id") } funcaoValid := true if funcaoUUID == uuid.Nil { funcaoValid = false } params := generated.UpdateProfissionalParams{ ID: pgtype.UUID{Bytes: uuidVal, Valid: true}, Nome: input.Nome, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid}, Endereco: toPgText(input.Endereco), Cidade: toPgText(input.Cidade), Uf: toPgText(input.Uf), Whatsapp: toPgText(input.Whatsapp), CpfCnpjTitular: toPgText(input.CpfCnpjTitular), Banco: toPgText(input.Banco), Agencia: toPgText(input.Agencia), Conta: toPgText(input.Conta), ContaPix: toPgText(input.ContaPix), CarroDisponivel: toPgBool(input.CarroDisponivel), TemEstudio: toPgBool(input.TemEstudio), QtdEstudio: toPgInt4(input.QtdEstudio), TipoCartao: toPgText(input.TipoCartao), Observacao: toPgText(input.Observacao), QualTec: toPgInt4(input.QualTec), EducacaoSimpatia: toPgInt4(input.EducacaoSimpatia), DesempenhoEvento: toPgInt4(input.DesempenhoEvento), DispHorario: toPgInt4(input.DispHorario), Media: toPgNumeric(input.Media), TabelaFree: toPgText(input.TabelaFree), ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento), Equipamentos: toPgText(input.Equipamentos), Email: toPgText(input.Email), AvatarUrl: toPgText(input.AvatarURL), Regiao: pgtype.Text{String: regiao, Valid: true}, } prof, err := s.queries.UpdateProfissional(ctx, params) if err != nil { return nil, err } // Update functions logic // If input.FuncoesIds is provided (even empty), replace all. // If nil, maybe keep existing? For simplicity, let's assume if present we update. // Actually frontend should send full list. if input.FuncoesIds != nil { // Clear existing _ = s.queries.ClearProfessionalFunctions(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true}) // Add new for _, fid := range input.FuncoesIds { fUUID, err := uuid.Parse(fid) if err == nil { _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ ProfissionalID: pgtype.UUID{Bytes: uuidVal, Valid: true}, FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true}, }) } } } else if input.FuncaoProfissionalID != "" { // If legacy field matches, ensure it's in junction set too? // Or maybe we treat legacy ID as primary and sync it. // For now, let's just make sure at least one exists if provided. // But usually update sends all data. } return &prof, nil } func (s *Service) GetByUserID(ctx context.Context, userID string, regiao string) (*generated.GetProfissionalByUsuarioIDRow, error) { uuidVal, err := uuid.Parse(userID) if err != nil { return nil, errors.New("invalid user id") } prof, err := s.queries.GetProfissionalByUsuarioID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true}) if err != nil { return nil, err } return &prof, nil } func (s *Service) Delete(ctx context.Context, id string, regiao string) error { fmt.Printf("[DEBUG] Deleting Professional: %s\n", id) uuidVal, err := uuid.Parse(id) if err != nil { return errors.New("invalid id") } // Get professional to find associated user prof, err := s.queries.GetProfissionalByID(ctx, generated.GetProfissionalByIDParams{ ID: pgtype.UUID{Bytes: uuidVal, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err != nil { fmt.Printf("[DEBUG] Failed to get professional %s: %v\n", id, err) return err } fmt.Printf("[DEBUG] Prof Found:: ID=%s, UsuarioID.Valid=%v, UsuarioID=%s\n", uuid.UUID(prof.ID.Bytes).String(), prof.UsuarioID.Valid, uuid.UUID(prof.UsuarioID.Bytes).String()) // Delete associated user first (ensures login access is revoked) if prof.UsuarioID.Valid { fmt.Printf("[DEBUG] Attempting to delete User ID: %s\n", uuid.UUID(prof.UsuarioID.Bytes).String()) err = s.queries.DeleteUsuario(ctx, prof.UsuarioID) if err != nil { fmt.Printf("[DEBUG] Failed to delete User: %v\n", err) return err } fmt.Println("[DEBUG] User deleted successfully.") } else { fmt.Println("[DEBUG] UsuarioID is invalid/null. Skipping User deletion.") } // Delete professional profile fmt.Println("[DEBUG] Deleting Professional profile...") err = s.queries.DeleteProfissional(ctx, generated.DeleteProfissionalParams{ ID: pgtype.UUID{Bytes: uuidVal, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err != nil { fmt.Printf("[DEBUG] Failed to delete Professional: %v\n", err) return err } fmt.Println("[DEBUG] Professional deleted successfully.") return nil } // Helpers func toPgText(s *string) pgtype.Text { if s == nil || *s == "" { return pgtype.Text{Valid: false} } return pgtype.Text{String: *s, Valid: true} } func toPgBool(b *bool) pgtype.Bool { if b == nil { return pgtype.Bool{Valid: false} } return pgtype.Bool{Bool: *b, Valid: true} } func toPgInt4(i *int) pgtype.Int4 { if i == nil { return pgtype.Int4{Valid: false} } return pgtype.Int4{Int32: int32(*i), Valid: true} } func toPgNumeric(f *float64) pgtype.Numeric { if f == nil { return pgtype.Numeric{Valid: false} } var n pgtype.Numeric if err := n.Scan(fmt.Sprintf("%f", *f)); err != nil { return pgtype.Numeric{Valid: false} } return n } // Import Logic type ImportProfessionalInput struct { CreateProfissionalInput // Any extra fields? Maybe not. } type ImportStats struct { Created int Updated int Errors int } func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput, regiao string) (ImportStats, []error) { stats := ImportStats{} var errs []error for i, input := range items { // 1. Validate / Normalize CPF if input.CpfCnpjTitular == nil || *input.CpfCnpjTitular == "" { errs = append(errs, fmt.Errorf("row %d: cpf is required", i)) stats.Errors++ continue } cpf := *input.CpfCnpjTitular // 2. Check if exists existing, err := s.queries.GetProfissionalByCPF(ctx, generated.GetProfissionalByCPFParams{ CpfCnpjTitular: pgtype.Text{String: cpf, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err == nil { // Found -> Update // Map input to UpdateProfissionalInput // Using a helper or manual mapping updateInput := UpdateProfissionalInput{ Nome: input.Nome, FuncaoProfissionalID: input.FuncaoProfissionalID, FuncoesIds: input.FuncoesIds, Endereco: input.Endereco, Cidade: input.Cidade, Uf: input.Uf, Whatsapp: input.Whatsapp, CpfCnpjTitular: input.CpfCnpjTitular, Banco: input.Banco, Agencia: input.Agencia, Conta: input.Conta, ContaPix: input.ContaPix, CarroDisponivel: input.CarroDisponivel, TemEstudio: input.TemEstudio, QtdEstudio: input.QtdEstudio, TipoCartao: input.TipoCartao, Observacao: input.Observacao, QualTec: input.QualTec, EducacaoSimpatia: input.EducacaoSimpatia, DesempenhoEvento: input.DesempenhoEvento, DispHorario: input.DispHorario, Media: input.Media, TabelaFree: input.TabelaFree, ExtraPorEquipamento: input.ExtraPorEquipamento, Equipamentos: input.Equipamentos, Email: input.Email, AvatarURL: input.AvatarURL, } // Use existing ID idStr := uuid.UUID(existing.ID.Bytes).String() _, err := s.Update(ctx, idStr, updateInput, regiao) if err != nil { errs = append(errs, fmt.Errorf("row %d (update): %v", i, err)) stats.Errors++ } else { stats.Updated++ } } else { // Not Found (or error) -> Check if it is Not Found // sqlc returns err on Not Found? likely pgx.ErrNoRows or similar // If it's real error, log it. But for now assume err means Not Found. // Create New // Use empty userID (or nil) // Create function takes userID string. // I need to adjust Create to accept nullable userID or pass "" and handle it. // Looking at Create code: // uuid.Parse(finalUserID). If "" -> error "invalid usuario_id" // So Create DOES NOT support empty user ID currently. // I need to Modify Create to support NULL user_id. // For now, I'll direct call queries.CreateProfissional here to bypass service check, OR fix Service.Create. // Fixing Service.Create is better. // Workaround: Call logic directly here for creation without User // Copy Logic from Create but allow UserID to be invalid/null var funcaoUUID uuid.UUID var funcaoValid bool if input.FuncaoProfissionalID != "" { parsed, err := uuid.Parse(input.FuncaoProfissionalID) if err == nil { funcaoUUID = parsed funcaoValid = true } } params := generated.CreateProfissionalParams{ UsuarioID: pgtype.UUID{Valid: false}, // Is NULL Nome: input.Nome, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid}, Endereco: toPgText(input.Endereco), Cidade: toPgText(input.Cidade), Uf: toPgText(input.Uf), Whatsapp: toPgText(input.Whatsapp), CpfCnpjTitular: toPgText(input.CpfCnpjTitular), Banco: toPgText(input.Banco), Agencia: toPgText(input.Agencia), Conta: toPgText(input.Conta), ContaPix: toPgText(input.ContaPix), CarroDisponivel: toPgBool(input.CarroDisponivel), TemEstudio: toPgBool(input.TemEstudio), QtdEstudio: toPgInt4(input.QtdEstudio), TipoCartao: toPgText(input.TipoCartao), Observacao: toPgText(input.Observacao), QualTec: toPgInt4(input.QualTec), EducacaoSimpatia: toPgInt4(input.EducacaoSimpatia), DesempenhoEvento: toPgInt4(input.DesempenhoEvento), DispHorario: toPgInt4(input.DispHorario), Media: toPgNumeric(input.Media), TabelaFree: toPgText(input.TabelaFree), ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento), Equipamentos: toPgText(input.Equipamentos), Email: toPgText(input.Email), AvatarUrl: toPgText(input.AvatarURL), Regiao: pgtype.Text{String: regiao, Valid: true}, } prof, err := s.queries.CreateProfissional(ctx, params) if err != nil { errs = append(errs, fmt.Errorf("row %d (create): %v", i, err)) stats.Errors++ } else { // Insert multiple functions if provided if len(input.FuncoesIds) > 0 { for _, fid := range input.FuncoesIds { fUUID, err := uuid.Parse(fid) if err == nil { _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true}, }) } } } else if funcaoValid { _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, FuncaoID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, }) } stats.Created++ } } } return stats, errs } // CheckExistence checks if a professional exists by CPF func (s *Service) CheckExistence(ctx context.Context, cpf string, regiao string) (exists bool, claimed bool, name string, err error) { prof, err := s.queries.GetProfissionalByCPF(ctx, generated.GetProfissionalByCPFParams{ CpfCnpjTitular: pgtype.Text{String: cpf, Valid: true}, Regiao: pgtype.Text{String: regiao, Valid: true}, }) if err != nil { if err.Error() == "no rows in result set" { return false, false, "", nil } return false, false, "", err } return true, prof.UsuarioID.Valid, prof.Nome, nil }