Files
tm_back/internal/customer/handler.go
T
Mazyar 22487eaef5 fix(user,customer): return 409 on phone duplicate-key races
Map E11000 from the per-collection phone index to friendly conflict
errors and skip redundant GetByPhone when the phone is unchanged.

Not blocking: IAT test DB may need a one-off dedupe for index build;
phone uniqueness is per collection (users vs customers), not global.
2026-06-06 20:34:45 +03:30

610 lines
25 KiB
Go

package customer
import (
"errors"
"strings"
"tm/internal/user"
"tm/pkg/authorization"
"tm/pkg/logger"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for customer operations
type Handler struct {
service Service
authService authorization.AuthorizationService
userHandler *user.Handler
logger logger.Logger
validator ValidationService
}
// NewHandler creates a new customer handler
func NewHandler(service Service, userHandler *user.Handler, authService authorization.AuthorizationService, logger logger.Logger, validator ValidationService) *Handler {
return &Handler{
service: service,
authService: authService,
userHandler: userHandler,
logger: logger,
validator: validator,
}
}
// CreateCustomer creates a new customer (Web Panel)
// @Summary Create a new customer
// @Description Create a new customer with comprehensive information including personal details, company information, address, and business details. This endpoint is used by the web panel for full customer registration.
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param customer body CreateCustomerForm true "Customer information including type (individual|company|government), personal details, company info, address, and business details"
// @Success 201 {object} response.APIResponse{data=CustomerResponse} "Customer created successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers [post]
func (h *Handler) CreateCustomer(c echo.Context) error {
form, err := response.Parse[CreateCustomerForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
customer, err := h.service.Register(c.Request().Context(), form)
if err != nil {
if isCustomerConflictError(err) {
return response.Conflict(c, err.Error())
}
if strings.HasPrefix(err.Error(), "invalid company ID") || err.Error() == "invalid username format" {
return response.BadRequest(c, err.Error(), "")
}
return response.InternalServerError(c, "Failed to create customer")
}
return response.Created(c, customer, "Customer created successfully")
}
// GetCustomerByID retrieves a customer by ID (Web Panel)
// @Summary Get customer by ID
// @Description Retrieve detailed customer information by customer ID
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [get]
func (h *Handler) GetCustomerByID(c echo.Context) error {
id := c.Param("id")
customer, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to get customer")
}
return response.Success(c, customer, "Customer retrieved successfully")
}
// UpdateCustomer updates a customer (Web Panel)
// @Summary Update customer information
// @Description Update customer information including personal details, company information, address, and business details
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param customer body UpdateCustomerForm true "Updated customer information"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [put]
func (h *Handler) UpdateCustomer(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[UpdateCustomerForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
customer, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
if isCustomerConflictError(err) {
return response.Conflict(c, err.Error())
}
if strings.HasPrefix(err.Error(), "invalid company ID") {
return response.BadRequest(c, err.Error(), err.Error())
}
return response.InternalServerError(c, "Failed to update customer")
}
return response.Success(c, customer, "Customer updated successfully")
}
// DeleteCustomer deletes a customer (Web Panel)
// @Summary Delete customer
// @Description Soft delete a customer by setting status to inactive
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse "Customer deleted successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [delete]
func (h *Handler) DeleteCustomer(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to delete customer")
}
return response.Success(c, map[string]interface{}{
"message": "Customer deleted successfully",
}, "Customer deleted successfully")
}
// ListCustomers lists customers with filters and pagination
// @Summary List customers with filters and pagination
// @Description Retrieve a paginated list of customers with filtering by full name, email, company, type, status, and role. Supports sorting and pagination.
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param q query string false "Search term to filter customers by email or username"
// @Param full_name query string false "Filter by customer full name (partial match)"
// @Param email query string false "Filter by customer email (partial match)"
// @Param type query string false "Filter by customer type" Enums(individual, company, government)
// @Param status query string false "Filter by customer status" Enums(active, inactive, suspended)
// @Param role query string false "Filter by customer role" Enums(admin, analyst)
// @Param company_id query string false "Filter by assigned company ID"
// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(email, company_name, created_at, updated_at, status) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CustomerListResponse} "Customers retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchCustomersForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list customers")
}
return response.SuccessWithMeta(c, result, result.Meta, "Customers retrieved successfully")
}
// UpdateCustomerStatus updates customer status (Web Panel)
// @Summary Update customer status
// @Description Update customer account status (active, inactive, suspended, pending)
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param status body UpdateStatusForm true "New customer status"
// @Success 200 {object} response.APIResponse "Customer status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/status [patch]
func (h *Handler) UpdateStatus(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[UpdateStatusForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
err = h.service.UpdateStatus(c.Request().Context(), id, form)
if err != nil {
return response.InternalServerError(c, "Failed to update customer status")
}
return response.Success(c, map[string]interface{}{
"message": "Customer status updated successfully",
}, "Customer status updated successfully")
}
// ResetCustomerPassword resets a customer's password (admin only).
// @Summary Reset customer password
// @Description Generate a new password for a customer. The plaintext is returned once for manual delivery.
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse{data=AdminResetPasswordResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 429 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/customers/{id}/reset-password [post]
func (h *Handler) ResetCustomerPassword(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "no-store")
actorID, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Unauthorized")
}
targetID := c.Param("id")
result, err := h.service.AdminResetPassword(c.Request().Context(), actorID, targetID)
if err != nil {
switch {
case errors.Is(err, errInvalidCustomerID):
return response.BadRequest(c, "Invalid id", "")
case errors.Is(err, errCustomerNotFound):
return response.NotFound(c, "Customer not found")
case errors.Is(err, errCannotResetCustPassword):
return response.Conflict(c, "Cannot reset password for this user")
case errors.Is(err, errCustResetPasswordRate):
return response.TooManyRequests(c, "Too many password reset requests")
default:
return response.InternalServerError(c, "Failed to reset password")
}
}
return response.Success(c, result, "Password has been reset successfully")
}
// AssignRole assigns a role to a customer (Web Panel)
// @Summary Assign role to customer
// @Description Assign a role (admin or analyst) to a customer
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param role body AssignRoleForm true "Role assignment information"
// @Success 200 {object} response.APIResponse{data=AssignRoleResponse} "Role assigned successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/role [patch]
func (h *Handler) AssignRole(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[AssignRoleForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.AssignRole(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
if err.Error() == "invalid role. Must be 'admin' or 'analyst'" {
return response.BadRequest(c, err.Error(), "")
}
return response.InternalServerError(c, "Failed to assign role to customer")
}
return response.Success(c, result, "Role assigned successfully")
}
// AssignCompanies assigns companies to a customer
// @Summary Assign companies to a customer
// @Description Assign companies to a customer
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param companies body AssignCompaniesForm true "Companies to assign"
// @Success 200 {object} response.APIResponse "Companies assigned successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/companies [patch]
func (h *Handler) AssignCompanies(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[AssignCompaniesForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
err = h.service.AssignCompanies(c.Request().Context(), id, form.Companies)
if err != nil {
return response.InternalServerError(c, "Failed to assign companies to customer")
}
return response.Success(c, map[string]interface{}{
"message": "Companies assigned successfully",
}, "Companies assigned successfully")
}
// **************************************************
// * PROFILE HANDLERS *
// **************************************************
// Login handles customer authentication
// @Summary Customer login
// @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.
// @Tags Authorization
// @Accept json
// @Produce json
// @Param login body LoginForm true "Login credentials (username/email and password)"
// @Success 200 {object} response.APIResponse{data=AuthResponse} "Login successful"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or inactive account"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/login [post]
func (h *Handler) Login(c echo.Context) error {
form, err := response.Parse[LoginForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Call service
authResponse, err := h.service.Login(c.Request().Context(), form)
if err != nil {
if err.Error() == "invalid credentials" {
return response.Unauthorized(c, err.Error())
}
if err.Error() == "account is not active" {
return response.Unauthorized(c, err.Error())
}
return response.InternalServerError(c, "Failed to authenticate customer")
}
return response.Success(c, authResponse, "Login successful")
}
// RefreshToken handles customer token refresh
// @Summary Refresh customer access token
// @Description Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.
// @Tags Authorization
// @Accept json
// @Produce json
// @Param refresh body RefreshTokenForm true "Refresh token for generating new access token"
// @Success 200 {object} response.APIResponse{data=AuthResponse} "Token refreshed successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid refresh token or feature not implemented"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error {
form, err := response.Parse[RefreshTokenForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Call service
authResponse, err := h.service.RefreshToken(c.Request().Context(), form.RefreshToken)
if err != nil {
if err.Error() == "token refresh not implemented yet" {
return response.BadRequest(c, "Token refresh not implemented yet", "")
}
return response.InternalServerError(c, "Failed to refresh token")
}
return response.Success(c, authResponse, "Token refreshed successfully")
}
// GetProfile retrieves customer profile information (Mobile)
// @Summary Get customer profile
// @Description Retrieve current customer profile information
// @Tags Authorization
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile [get]
func (h *Handler) GetProfile(c echo.Context) error {
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer not authenticated")
}
resp, err := h.service.GetProfileWithCompanies(c.Request().Context(), customerID)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to get customer profile")
}
return response.Success(c, resp, "Profile retrieved successfully")
}
// UpdateProfileKeywords updates customer profile keywords (Mobile)
// @Summary Update customer profile keywords
// @Description Update profile keywords manually; optional async keyword refresh is reserved for the AI service.
// @Tags Authorization
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body UpdateProfileKeywordsForm true "Manual keywords"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile keywords update started"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/keywords [patch]
func (h *Handler) UpdateProfileKeywords(c echo.Context) error {
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer not authenticated")
}
form, err := response.Parse[UpdateProfileKeywordsForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
resp, err := h.service.UpdateProfileKeywords(c.Request().Context(), customerID, form.Keywords)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to update profile keywords")
}
return response.Success(c, resp, "Profile keywords update started")
}
// Logout handles customer logout (Mobile)
// @Summary Customer logout
// @Description Logout customer and invalidate access token
// @Tags Authorization
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse "Logout successful"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/logout [delete]
func (h *Handler) Logout(c echo.Context) error {
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer not authenticated")
}
// Get access token from context
accessToken, err := GetAccessTokenFromContext(c)
if err != nil {
return response.Unauthorized(c, "Access token not found")
}
err = h.service.Logout(c.Request().Context(), customerID, accessToken)
if err != nil {
// Don't return error here as the customer should still be logged out
h.logger.Warn("Failed to invalidate access token during logout", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
}
return response.Success(c, map[string]interface{}{
"message": "Logout successful",
}, "Logout successful")
}
// RequestResetPassword handles password reset request
// @Summary Request password reset
// @Description Send a password reset OTP code to the customer's email address
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body RequestResetPasswordForm true "Email address for password reset"
// @Success 200 {object} response.APIResponse{data=RequestResetPasswordResponse} "Password reset code sent successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/forgot-password [post]
func (h *Handler) RequestResetPassword(c echo.Context) error {
form, err := response.Parse[RequestResetPasswordForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.RequestResetPassword(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to process password reset request")
}
return response.Success(c, result, "Password reset request processed")
}
// VerifyOTP handles OTP verification for password reset
// @Summary Verify OTP code
// @Description Verify the OTP code received via email and get a reset token
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body VerifyOTPForm true "Email and OTP code for verification"
// @Success 200 {object} response.APIResponse{data=VerifyOTPResponse} "OTP verified successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired OTP"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/verify-otp [post]
func (h *Handler) VerifyOTP(c echo.Context) error {
form, err := response.Parse[VerifyOTPForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.VerifyOTP(c.Request().Context(), form)
if err != nil {
return response.Unauthorized(c, "Invalid or expired verification code")
}
return response.Success(c, result, "OTP verified successfully")
}
// ResetPassword handles password reset with token
// @Summary Reset password
// @Description Reset the customer's password using the reset token
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body ResetPasswordForm true "Email, reset token, and new password"
// @Success 200 {object} response.APIResponse{data=ResetPasswordResponse} "Password reset successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired reset token"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/reset-password [post]
func (h *Handler) ResetPassword(c echo.Context) error {
form, err := response.Parse[ResetPasswordForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ResetPassword(c.Request().Context(), form)
if err != nil {
return response.Unauthorized(c, err.Error())
}
return response.Success(c, result, "Password reset successfully")
}