800 lines
29 KiB
Go
800 lines
29 KiB
Go
package profissionais
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"photum-backend/internal/db/generated"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
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"`
|
|
Cep *string `json:"cep"`
|
|
Numero *string `json:"numero"`
|
|
Complemento *string `json:"complemento"`
|
|
Bairro *string `json:"bairro"`
|
|
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),
|
|
Cep: mergeStr(input.Cep, existing.Cep),
|
|
Numero: mergeStr(input.Numero, existing.Numero),
|
|
Complemento: mergeStr(input.Complemento, existing.Complemento),
|
|
Bairro: mergeStr(input.Bairro, existing.Bairro),
|
|
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),
|
|
Cep: toPgText(input.Cep),
|
|
Numero: toPgText(input.Numero),
|
|
Complemento: toPgText(input.Complemento),
|
|
Bairro: toPgText(input.Bairro),
|
|
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"`
|
|
Endereco *string `json:"endereco"`
|
|
Cidade *string `json:"cidade"`
|
|
Uf *string `json:"uf"`
|
|
Cep *string `json:"cep"`
|
|
Numero *string `json:"numero"`
|
|
Complemento *string `json:"complemento"`
|
|
Bairro *string `json:"bairro"`
|
|
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"`
|
|
Senha *string `json:"senha"` // New field for password update
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// 1. Password Update Logic (if provided)
|
|
if input.Senha != nil && *input.Senha != "" {
|
|
fmt.Printf("[DEBUG] Updating password for professional %s. New Password Length: %d\n", id, len(*input.Senha))
|
|
|
|
// Get Professional to find UsuarioID
|
|
// Requires region to be safe, though ID is unique. Using passed region.
|
|
currentProf, 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] Error fetching professional for password update: %v\n", err)
|
|
} else {
|
|
fmt.Printf("[DEBUG] Professional found. UsuarioID Valid: %v, UUID: %v\n", currentProf.UsuarioID.Valid, currentProf.UsuarioID.Bytes)
|
|
}
|
|
|
|
if err == nil && currentProf.UsuarioID.Valid {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*input.Senha), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("erro ao gerar hash da senha: %v", err)
|
|
}
|
|
|
|
fmt.Printf("[DEBUG] Updating UsuarioID %v with new hash\n", currentProf.UsuarioID.Bytes)
|
|
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
|
|
ID: currentProf.UsuarioID,
|
|
SenhaHash: string(hashedPassword),
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("[DEBUG] Error updating user password: %v\n", err)
|
|
return nil, fmt.Errorf("erro ao atualizar senha do usuário: %v", err)
|
|
}
|
|
fmt.Println("[DEBUG] Password updated successfully")
|
|
} else {
|
|
// No UsuarioID found. We need to create one or link to existing email.
|
|
emailToUse := input.Email
|
|
if emailToUse == nil || *emailToUse == "" {
|
|
if currentProf.Email.Valid {
|
|
e := currentProf.Email.String
|
|
emailToUse = &e
|
|
}
|
|
}
|
|
|
|
if emailToUse != nil && *emailToUse != "" {
|
|
fmt.Printf("[DEBUG] User not linked. Attempting to link/create for email: %s\n", *emailToUse)
|
|
|
|
// 1. Check if user exists
|
|
existingUser, err := s.queries.GetUsuarioByEmail(ctx, *emailToUse)
|
|
var userID pgtype.UUID
|
|
|
|
hashedPassword, hashErr := bcrypt.GenerateFromPassword([]byte(*input.Senha), bcrypt.DefaultCost)
|
|
if hashErr != nil {
|
|
return nil, fmt.Errorf("erro ao gerar hash da senha: %v", hashErr)
|
|
}
|
|
|
|
if err == nil {
|
|
// User exists. Link it and update password.
|
|
fmt.Printf("[DEBUG] User exists with ID %v. Linking...\n", existingUser.ID.Bytes)
|
|
userID = existingUser.ID
|
|
|
|
// Update password for existing user
|
|
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
|
|
ID: userID,
|
|
SenhaHash: string(hashedPassword),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("erro ao atualizar senha do usuário existente: %v", err)
|
|
}
|
|
} else {
|
|
// User does not exist. Create new user.
|
|
fmt.Println("[DEBUG] User does not exist. Creating new user...")
|
|
|
|
// Determine role based on function. Default to PHOTOGRAPHER.
|
|
role := "PHOTOGRAPHER" // Default
|
|
// Ideally we should use the function to determine role, but for now safe default or "RESEARCHER" check?
|
|
// input.FuncaoProfissionalID/FuncoesIds might be present.
|
|
// Let's rely on default for now as we don't have easy access to role logic here without circular dependency or extra queries.
|
|
// Actually, we can just set it to PHOTOGRAPHER as it grants access to app. They need proper access.
|
|
|
|
newUser, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
|
Email: *emailToUse,
|
|
SenhaHash: string(hashedPassword),
|
|
Role: role,
|
|
TipoProfissional: pgtype.Text{String: "Fotógrafo", Valid: true}, // Placeholder, should be aligned with function
|
|
Ativo: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("erro ao criar novo usuário para profissional: %v", err)
|
|
}
|
|
userID = newUser.ID
|
|
fmt.Printf("[DEBUG] Created new user with ID %v\n", userID.Bytes)
|
|
|
|
// Ensure region access
|
|
if regiao != "" {
|
|
_ = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
|
|
ID: userID,
|
|
RegioesPermitidas: []string{regiao},
|
|
})
|
|
}
|
|
}
|
|
|
|
// Link Professional to User
|
|
err = s.queries.LinkProfissionalToUsuario(ctx, generated.LinkProfissionalToUsuarioParams{
|
|
ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
|
|
UsuarioID: userID,
|
|
Regiao: pgtype.Text{String: regiao, Valid: true},
|
|
})
|
|
if err != nil {
|
|
// If link fails, we might leave a dangling user if created, but that's acceptable for now.
|
|
return nil, fmt.Errorf("erro ao vincular profissional ao usuário: %v", err)
|
|
}
|
|
fmt.Println("[DEBUG] Professional successfully linked to User.")
|
|
|
|
} else {
|
|
fmt.Println("[DEBUG] Cannot create user: No email available for professional.")
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println("[DEBUG] No password provided for update")
|
|
}
|
|
|
|
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),
|
|
Cep: toPgText(input.Cep),
|
|
Numero: toPgText(input.Numero),
|
|
Complemento: toPgText(input.Complemento),
|
|
Bairro: toPgText(input.Bairro),
|
|
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
|
|
}
|