package handlers import ( "context" "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/timetracker/backend/internal/api/dto" "github.com/timetracker/backend/internal/api/middleware" "github.com/timetracker/backend/internal/api/responses" "github.com/timetracker/backend/internal/api/utils" "github.com/timetracker/backend/internal/models" "github.com/timetracker/backend/internal/types" ) // CustomerHandler handles customer-related API endpoints type CustomerHandler struct{} // NewCustomerHandler creates a new CustomerHandler func NewCustomerHandler() *CustomerHandler { return &CustomerHandler{} } // GetCustomers handles GET /customers // // @Summary Get all customers // @Description Get a list of all customers // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} utils.Response{data=[]dto.CustomerDto} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers [get] func (h *CustomerHandler) GetCustomers(c *gin.Context) { utils.HandleGetAll(c, models.GetAllCustomers, convertCustomerToDTO, "customers") } // GetCustomerByID handles GET /customers/:id // // @Summary Get customer by ID // @Description Get a customer by its ID // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Customer ID" // @Success 200 {object} utils.Response{data=dto.CustomerDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 404 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers/{id} [get] func (h *CustomerHandler) GetCustomerByID(c *gin.Context) { utils.HandleGetByID(c, models.GetCustomerByID, convertCustomerToDTO, "customer") } // GetCustomersByCompanyID handles GET /customers/company/:companyId // // @Summary Get customers by company ID // @Description Get a list of customers for a specific company // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Param companyId path int true "Company ID" // @Success 200 {object} utils.Response{data=[]dto.CustomerDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers/company/{companyId} [get] func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) { // Parse company ID from URL companyIDStr := c.Param("companyId") companyID, err := parseCompanyID(companyIDStr) if err != nil { responses.BadRequestResponse(c, "Invalid company ID format") return } // Create a wrapper function that takes a ULID but calls the original function with an int getByCompanyIDFn := func(ctx context.Context, _ types.ULID) ([]models.Customer, error) { return models.GetCustomersByCompanyID(ctx, companyID) } // Get customers from the database and convert to DTOs customers, err := getByCompanyIDFn(c.Request.Context(), types.ULID{}) if err != nil { responses.InternalErrorResponse(c, "Error retrieving customers: "+err.Error()) return } // Convert to DTOs customerDTOs := utils.ConvertToDTO(customers, convertCustomerToDTO) responses.SuccessResponse(c, http.StatusOK, customerDTOs) } // CreateCustomer handles POST /customers // // @Summary Create a new customer // @Description Create a new customer // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Param customer body dto.CustomerCreateDto true "Customer data" // @Success 201 {object} utils.Response{data=dto.CustomerDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers [post] func (h *CustomerHandler) CreateCustomer(c *gin.Context) { // We need to use a custom wrapper for CreateCustomer because we need to get the user ID from the context userID, err := middleware.GetUserIDFromContext(c) if err != nil { responses.UnauthorizedResponse(c, "User not authenticated") return } // Use a closure to capture the userID createFn := func(ctx context.Context, createDTO dto.CustomerCreateDto) (*models.Customer, error) { return createCustomerWrapper(ctx, createDTO, userID) } utils.HandleCreate(c, createFn, convertCustomerToDTO, "customer") } // UpdateCustomer handles PUT /customers/:id // // @Summary Update a customer // @Description Update an existing customer // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Customer ID" // @Param customer body dto.CustomerUpdateDto true "Customer data" // @Success 200 {object} utils.Response{data=dto.CustomerDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 404 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers/{id} [put] func (h *CustomerHandler) UpdateCustomer(c *gin.Context) { utils.HandleUpdate(c, models.UpdateCustomer, convertCustomerToDTO, prepareCustomerUpdate, "customer") } // DeleteCustomer handles DELETE /customers/:id // // @Summary Delete a customer // @Description Delete a customer by its ID // @Tags customers // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Customer ID" // @Success 204 {object} utils.Response // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /customers/{id} [delete] func (h *CustomerHandler) DeleteCustomer(c *gin.Context) { utils.HandleDelete(c, models.DeleteCustomer, "customer") } // Helper functions for DTO conversion func convertCustomerToDTO(customer *models.Customer) dto.CustomerDto { var companyID *string if customer.CompanyID != nil { s := customer.CompanyID.String() companyID = &s } return dto.CustomerDto{ ID: customer.ID.String(), CreatedAt: customer.CreatedAt, UpdatedAt: customer.UpdatedAt, Name: customer.Name, CompanyID: companyID, } } func convertCreateCustomerDTOToModel(dto dto.CustomerCreateDto) (models.CustomerCreate, error) { var companyID *types.ULID if dto.CompanyID != nil { wrapper, err := types.ULIDFromString(*dto.CompanyID) // Ignoring error, validation happens in the model if err != nil { return models.CustomerCreate{}, fmt.Errorf("invalid company ID: %w", err) } companyID = &wrapper } create := models.CustomerCreate{ Name: dto.Name, CompanyID: companyID, } return create, nil } func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) (models.CustomerUpdate, error) { id, err := types.ULIDFromString(dto.ID) if err != nil { return models.CustomerUpdate{}, fmt.Errorf("invalid customer ID: %w", err) } update := models.CustomerUpdate{ ID: id, } if dto.Name != nil { update.Name = dto.Name } if dto.CompanyID.Valid { companyID, err := types.ULIDFromString(*dto.CompanyID.Value) if err != nil { return models.CustomerUpdate{}, fmt.Errorf("invalid company ID: %w", err) } update.CompanyID = &companyID } else { update.CompanyID = nil } return update, nil } // Helper function to parse company ID from string func parseCompanyID(idStr string) (int, error) { var id int _, err := fmt.Sscanf(idStr, "%d", &id) return id, err } // prepareCustomerUpdate prepares the customer update object by parsing the ID, binding the JSON, and converting the DTO to a model func prepareCustomerUpdate(c *gin.Context) (models.CustomerUpdate, error) { // Parse ID from URL id, err := utils.ParseID(c, "id") if err != nil { responses.BadRequestResponse(c, "Invalid customer ID format") return models.CustomerUpdate{}, err } // Parse request body var customerUpdateDTO dto.CustomerUpdateDto if err := utils.BindJSON(c, &customerUpdateDTO); err != nil { responses.BadRequestResponse(c, err.Error()) return models.CustomerUpdate{}, err } // Set ID from URL customerUpdateDTO.ID = id.String() // Convert DTO to model customerUpdate, err := convertUpdateCustomerDTOToModel(customerUpdateDTO) if err != nil { responses.BadRequestResponse(c, "Invalid request body: "+err.Error()) return models.CustomerUpdate{}, err } return customerUpdate, nil } // createCustomerWrapper is a wrapper function for models.CreateCustomer that takes a DTO as input func createCustomerWrapper(ctx context.Context, createDTO dto.CustomerCreateDto, userID types.ULID) (*models.Customer, error) { // Convert DTO to model customerCreate, err := convertCreateCustomerDTOToModel(createDTO) if err != nil { return nil, err } // Set the owner user ID customerCreate.OwnerUserID = &userID // Call the original function return models.CreateCustomer(ctx, customerCreate) }