Enhance API documentation and restructure routes for improved clarity

- Updated README.md to include comprehensive Swagger documentation details, highlighting new features and endpoint categories.
- Introduced a new router structure to streamline route registration for admin and public endpoints, enhancing maintainability.
- Updated health check endpoint documentation to reflect comprehensive server status information.
- Enhanced customer and company management routes with improved descriptions and examples in Swagger.
- Refactored customer and user handlers to remove obsolete route registrations, aligning with the new router structure.
- Improved response handling in customer and user services to utilize string IDs consistently.
- Updated validation rules and forms for customer management to support multiple company assignments.
- Enhanced logging practices across services to ensure better traceability and error handling.
This commit is contained in:
n.nakhostin
2025-08-11 18:24:34 +03:30
parent 566fa07574
commit 3e4831c2e7
23 changed files with 3605 additions and 2087 deletions
+51 -30
View File
@@ -2,6 +2,8 @@ package customer
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// CustomerStatus represents customer account status
@@ -26,9 +28,9 @@ const (
// Customer represents a customer in the tender management system
// A customer can have an associated company for login purposes
type Customer struct {
mongo.Model
Type CustomerType `bson:"type" json:"type"`
Status CustomerStatus `bson:"status" json:"status"`
mongo.Model `bson:",inline"`
Type CustomerType `bson:"type" json:"type"`
Status CustomerStatus `bson:"status" json:"status"`
// Individual customer fields
FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"`
@@ -40,8 +42,10 @@ type Customer struct {
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
// Company customer fields (for when customer represents a company)
CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"`
// Company relationships - each customer can be assigned to multiple companies
CompanyIDs []string `bson:"company_ids,omitempty" json:"company_ids,omitempty"`
// Company customer fields
RegistrationNumber *string `bson:"registration_number,omitempty" json:"registration_number,omitempty"`
TaxID *string `bson:"tax_id,omitempty" json:"tax_id,omitempty"`
Industry *string `bson:"industry,omitempty" json:"industry,omitempty"`
@@ -75,12 +79,12 @@ type Customer struct {
// SetID sets the customer ID (implements IDSetter interface)
func (c *Customer) SetID(id string) {
c.ID = id
c.ID, _ = primitive.ObjectIDFromHex(id)
}
// GetID returns the customer ID (implements IDGetter interface)
func (c *Customer) GetID() string {
return c.ID
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
@@ -126,44 +130,54 @@ type ContactPerson struct {
IsPrimary bool `bson:"is_primary" json:"is_primary"`
}
// CompanySummary represents company summary data for customer responses
type CompanySummary struct {
ID string `json:"id"`
Name string `json:"name"`
}
// CustomerResponse represents the customer data sent in API responses
type CustomerResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
FullName *string `json:"full_name,omitempty"`
Email string `json:"email"`
Phone *string `json:"phone,omitempty"`
Mobile *string `json:"mobile,omitempty"`
CompanyID *string `json:"company_id,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
FullName *string `json:"full_name,omitempty"`
Email string `json:"email"`
Phone *string `json:"phone,omitempty"`
Mobile *string `json:"mobile,omitempty"`
// Company relationships
Companies []*CompanySummary `json:"companies,omitempty"`
// Company customer fields
RegistrationNumber *string `json:"registration_number,omitempty"`
TaxID *string `json:"tax_id,omitempty"`
Industry *string `json:"industry,omitempty"`
Address *Address `json:"address,omitempty"`
BusinessType *string `json:"business_type,omitempty"`
EmployeeCount *int `json:"employee_count,omitempty"`
AnnualRevenue *float64 `json:"annual_revenue,omitempty"`
FoundedYear *int `json:"founded_year,omitempty"`
ContactPerson *ContactPerson `json:"contact_person,omitempty"`
TaxID *string `json:"tax_id"`
Industry *string `json:"industry"`
Address *Address `json:"address"`
BusinessType *string `json:"business_type"`
EmployeeCount *int `json:"employee_count"`
AnnualRevenue *float64 `json:"annual_revenue"`
FoundedYear *int `json:"founded_year"`
ContactPerson *ContactPerson `json:"contact_person"`
IsVerified bool `json:"is_verified"`
IsCompliant bool `json:"is_compliant"`
ComplianceNotes *string `json:"compliance_notes,omitempty"`
ComplianceNotes *string `json:"compliance_notes"`
Language string `json:"language"`
Username string `json:"username"`
Currency string `json:"currency"`
Timezone string `json:"timezone"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CreatedBy *string `json:"created_by,omitempty"`
UpdatedBy *string `json:"updated_by,omitempty"`
CreatedBy *string `json:"created_by"`
UpdatedBy *string `json:"updated_by"`
}
// ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse {
return &CustomerResponse{
ID: c.ID,
ID: c.ID.Hex(),
Type: string(c.Type),
Status: string(c.Status),
FirstName: c.FirstName,
@@ -173,7 +187,7 @@ func (c *Customer) ToResponse() *CustomerResponse {
Email: c.Email,
Phone: c.Phone,
Mobile: c.Mobile,
CompanyID: c.CompanyID,
Companies: nil, // Will be populated by service layer
RegistrationNumber: c.RegistrationNumber,
TaxID: c.TaxID,
Industry: c.Industry,
@@ -195,3 +209,10 @@ func (c *Customer) ToResponse() *CustomerResponse {
UpdatedBy: c.UpdatedBy,
}
}
// ToResponseWithCompanies converts Customer to CustomerResponse with company information
func (c *Customer) ToResponseWithCompanies(companies []*CompanySummary) *CustomerResponse {
response := c.ToResponse()
response.Companies = companies
return response
}
+25 -8
View File
@@ -2,8 +2,10 @@ package customer
// CreateCustomerForm represents the form for creating a new customer
type CreateCustomerForm struct {
Type string `json:"type" valid:"required,in(individual|company|government)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Type string `json:"type" valid:"required,in(individual|company|government)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
// Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
@@ -41,8 +43,10 @@ type CreateCustomerForm struct {
// UpdateCustomerForm represents the form for updating a customer
type UpdateCustomerForm struct {
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
// Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
@@ -154,8 +158,10 @@ type CreateCustomerMobileForm struct {
Password string `json:"password" valid:"required,length(8|128)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
}
type UpdateCustomerMobileForm struct {
@@ -164,8 +170,10 @@ type UpdateCustomerMobileForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
}
// Customer Authentication DTOs
@@ -204,3 +212,12 @@ type AuthResponse struct {
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
// Company assignment forms
type AssignCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
type RemoveCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
+181 -35
View File
@@ -28,38 +28,6 @@ func NewHandler(service Service, userHandler *user.Handler, authService authoriz
}
}
// RegisterRoutes registers customer routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
adminV1 := e.Group("/admin/v1/customers")
adminV1.Use(h.userHandler.AuthMiddleware())
adminV1.POST("", h.CreateCustomer)
adminV1.GET("", h.ListCustomers)
adminV1.GET("/:id", h.GetCustomerByID)
adminV1.PUT("/:id", h.UpdateCustomer)
adminV1.DELETE("/:id", h.DeleteCustomer)
adminV1.PATCH("/:id/status", h.UpdateCustomerStatus)
adminV1.PATCH("/:id/verification", h.UpdateCustomerVerification)
adminV1.POST("/:id/verify", h.VerifyCustomer)
adminV1.POST("/:id/suspend", h.SuspendCustomer)
adminV1.POST("/:id/activate", h.ActivateCustomer)
adminV1.GET("/company/:companyId", h.GetCustomersByCompanyID)
adminV1.GET("/type/:type", h.GetCustomersByType)
adminV1.GET("/status/:status", h.GetCustomersByStatus)
// Mobile-specific endpoints
v1 := e.Group("/api/v1")
authorizationGP := v1.Group("")
authorizationGP.POST("/login", h.Login)
authorizationGP.POST("/refresh-token", h.RefreshToken)
profileGP := v1.Group("")
profileGP.Use(h.AuthMiddleware())
profileGP.GET("/profile", h.GetProfile)
profileGP.DELETE("/logout", h.Logout)
}
// 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.
@@ -127,6 +95,33 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
return response.Success(c, customer.ToResponse(), "Customer retrieved successfully")
}
// GetCustomerByIDWithCompanies retrieves a customer by ID with companies (Web Panel)
// @Summary Get customer by ID with companies
// @Description Retrieve detailed customer information by customer ID including assigned companies
// @Tags Customers-Admin
// @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}/with-companies [get]
func (h *Handler) GetCustomerByIDWithCompanies(c echo.Context) error {
idStr := c.Param("id")
customer, err := h.service.GetCustomerByIDWithCompanies(c.Request().Context(), idStr)
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
@@ -247,6 +242,45 @@ func (h *Handler) ListCustomers(c echo.Context) error {
return response.Success(c, result, "Customers retrieved successfully")
}
// ListCustomersWithCompanies lists customers with companies and filters
// @Summary List customers with companies and filters
// @Description Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param search query string false "Search term to filter customers by name, email, or company name"
// @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, pending)
// @Param company_id query string false "Filter by company ID" format(uuid)
// @Param industry query string false "Filter by industry"
// @Param is_verified query boolean false "Filter by verification status"
// @Param is_compliant query boolean false "Filter by compliance status"
// @Param language query string false "Filter by preferred language"
// @Param currency query string false "Filter by preferred currency"
// @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 with companies 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/with-companies [get]
func (h *Handler) ListCustomersWithCompanies(c echo.Context) error {
form, err := response.Parse[ListCustomersForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to list customers with companies")
}
return response.Success(c, result, "Customers with companies retrieved successfully")
}
// UpdateCustomerStatus updates customer status (Web Panel)
// @Summary Update customer status
// @Description Update customer account status (active, inactive, suspended, pending)
@@ -604,6 +638,88 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully")
}
// AssignCompaniesToCustomer assigns companies to a customer (Web Panel)
// @Summary Assign companies to a customer
// @Description Assign one or more companies to a specific customer.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param companies body AssignCompaniesForm true "List of company IDs to assign"
// @Success 200 {object} response.APIResponse "Companies 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}/companies/assign [post]
func (h *Handler) AssignCompaniesToCustomer(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[AssignCompaniesForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
assignedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.AssignCompaniesToCustomer(c.Request().Context(), idStr, form.CompanyIDs, &assignedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to assign companies to customer")
}
return response.Success(c, map[string]interface{}{
"message": "Companies assigned successfully",
}, "Companies assigned successfully")
}
// RemoveCompaniesFromCustomer removes companies from a customer (Web Panel)
// @Summary Remove companies from a customer
// @Description Remove one or more companies from a specific customer.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param companies body RemoveCompaniesForm true "List of company IDs to remove"
// @Success 200 {object} response.APIResponse "Companies removed 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}/companies/remove [post]
func (h *Handler) RemoveCompaniesFromCustomer(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[RemoveCompaniesForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
removedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.RemoveCompaniesFromCustomer(c.Request().Context(), idStr, form.CompanyIDs, &removedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to remove companies from customer")
}
return response.Success(c, map[string]interface{}{
"message": "Companies removed successfully",
}, "Companies removed successfully")
}
// 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.
@@ -616,7 +732,7 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
// @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/login [post]
// @Router /api/v1/profile/login [post]
func (h *Handler) Login(c echo.Context) error {
form, err := response.Parse[LoginForm](c)
if err != nil {
@@ -650,7 +766,7 @@ func (h *Handler) Login(c echo.Context) error {
// @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/refresh-token [post]
// @Router /api/v1/profile/refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error {
form, err := response.Parse[RefreshTokenForm](c)
if err != nil {
@@ -680,7 +796,7 @@ func (h *Handler) RefreshToken(c echo.Context) error {
// @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]
// @Router /api/v1/ [get]
func (h *Handler) GetProfile(c echo.Context) error {
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
@@ -699,6 +815,36 @@ func (h *Handler) GetProfile(c echo.Context) error {
return response.Success(c, customer.ToResponse(), "Profile retrieved successfully")
}
// GetProfileWithCompanies retrieves customer profile with companies (Mobile)
// @Summary Get customer profile with companies
// @Description Retrieve current customer profile information along with their assigned companies.
// @Tags Customers-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/with-companies [get]
func (h *Handler) GetProfileWithCompanies(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")
}
customer, 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 with companies")
}
return response.Success(c, customer, "Profile retrieved successfully")
}
// Logout handles customer logout (Mobile)
// @Summary Customer logout
// @Description Logout customer and invalidate access token
+66 -1
View File
@@ -20,11 +20,13 @@ type Repository interface {
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error)
GetByTaxID(ctx context.Context, taxID string) (*Customer, error)
GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error)
GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error)
Update(ctx context.Context, customer *Customer) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*Customer, error)
Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
CountByCompanyID(ctx context.Context, companyID string) (int64, error)
CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error)
CountByType(ctx context.Context, customerType CustomerType) (int64, error)
CountByStatus(ctx context.Context, status CustomerStatus) (int64, error)
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
@@ -46,7 +48,7 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
*mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", Value: 1}}),
*mongopkg.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
*mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
*mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
*mongopkg.NewIndex("company_ids_idx", bson.D{{Key: "company_ids", Value: 1}}), // Index for CompanyIDs array
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
@@ -245,6 +247,46 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin
return customers, nil
}
// GetByCompanyIDs retrieves customers by multiple company IDs with pagination
func (r *customerRepository) GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) {
if len(companyIDs) == 0 {
return nil, errors.New("no company IDs provided")
}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Filter by company IDs and active status
filter := bson.M{
"company_ids": bson.M{"$in": companyIDs},
"status": bson.M{"$ne": CustomerStatusInactive},
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get customers by multiple company IDs", map[string]interface{}{
"error": err.Error(),
"company_ids": companyIDs,
"limit": limit,
"offset": offset,
})
return nil, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, nil
}
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
// Set updated timestamp using Unix timestamp
@@ -424,6 +466,29 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID str
return count, nil
}
// CountByCompanyIDs counts customers by multiple company IDs
func (r *customerRepository) CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) {
if len(companyIDs) == 0 {
return 0, errors.New("no company IDs provided")
}
filter := bson.M{
"company_ids": bson.M{"$in": companyIDs},
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by multiple company IDs", map[string]interface{}{
"error": err.Error(),
"company_ids": companyIDs,
})
return 0, err
}
return count, nil
}
// CountByType counts customers by type
func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) {
filter := bson.M{
+350 -24
View File
@@ -5,11 +5,12 @@ import (
"errors"
"time"
"tm/internal/company"
"tm/pkg/logger"
"tm/pkg/authorization"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/crypto/bcrypt"
)
@@ -18,11 +19,13 @@ type Service interface {
// Core customer operations
CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error)
GetCustomerByID(ctx context.Context, id string) (*Customer, error)
GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error)
DeleteCustomer(ctx context.Context, id string, deletedBy *string) error
// Customer listing and search
ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error)
ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error)
GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error)
GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error)
GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error)
@@ -31,6 +34,10 @@ type Service interface {
UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error
UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error
// Company assignments
AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error
RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error
// Mobile-specific operations (simplified)
CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error)
UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error)
@@ -44,22 +51,25 @@ type Service interface {
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfile(ctx context.Context, customerID string) (*Customer, error)
GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error)
Logout(ctx context.Context, customerID string, accessToken string) error
}
// customerService implements the Service interface
type customerService struct {
repository Repository
logger logger.Logger
authService authorization.AuthorizationService
repository Repository
logger logger.Logger
authService authorization.AuthorizationService
companyService company.Service
}
// NewCustomerService creates a new customer service
func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService) Service {
func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service {
return &customerService{
repository: repository,
logger: logger,
authService: authService,
repository: repository,
logger: logger,
authService: authService,
companyService: companyService,
}
}
@@ -95,10 +105,21 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
}
}
// Parse company ID if provided
var companyID *string
if form.CompanyID != nil {
companyID = form.CompanyID
// Prepare company assignments
var companyIDs []string
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
}
companyIDs = form.CompanyIDs
}
// Hash password
@@ -114,7 +135,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
customer := &Customer{
Type: CustomerType(form.Type),
Status: CustomerStatusPending,
CompanyID: companyID,
CompanyIDs: companyIDs,
FirstName: form.FirstName,
LastName: form.LastName,
FullName: form.FullName,
@@ -155,6 +176,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy,
})
@@ -180,6 +202,38 @@ func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Cust
return customer, nil
}
// GetCustomerByIDWithCompanies retrieves a customer by ID and loads associated companies
func (s *customerService) GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer by ID with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, err
}
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
s.logger.Info("Customer retrieved with companies successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return customerResponse, nil
}
// UpdateCustomer updates a customer
func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) {
// Get existing customer
@@ -233,8 +287,22 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.Type = CustomerType(*form.Type)
}
if form.CompanyID != nil {
customer.CompanyID = form.CompanyID
// Handle company assignments
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
var validCompanyIDs []string
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in update", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
customer.CompanyIDs = validCompanyIDs
}
if form.FirstName != nil {
@@ -411,6 +479,79 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers
}, nil
}
// ListCustomersWithCompanies lists customers with search and filters, including companies
func (s *customerService) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
search := ""
if form.Search != nil {
search = *form.Search
}
sortBy := "created_at"
if form.SortBy != nil {
sortBy = *form.SortBy
}
sortOrder := "desc"
if form.SortOrder != nil {
sortOrder = *form.SortOrder
}
// Parse company ID if provided
var companyID *string
if form.CompanyID != nil {
companyID = form.CompanyID
}
// Get customers
customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder)
if err != nil {
s.logger.Error("Failed to list customers with companies", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to list customers with companies")
}
// Convert to responses
var customerResponses []*CustomerResponse
for _, customer := range customers {
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer in list", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
customerResponses = append(customerResponses, customerResponse)
}
// TODO: Implement proper count for pagination metadata
total := int64(len(customers)) // This should be a separate count query
totalPages := (total + int64(limit) - 1) / int64(limit)
return &CustomerListResponse{
Customers: customerResponses,
Total: total,
Limit: limit,
Offset: offset,
TotalPages: int(totalPages),
}, nil
}
// GetCustomersByCompanyID retrieves customers by company ID with pagination
func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) {
customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset)
@@ -546,6 +687,98 @@ func (s *customerService) UpdateCustomerVerification(ctx context.Context, id str
return nil
}
// AssignCompaniesToCustomer assigns companies to a customer
func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Verify companies exist and update customer's company IDs
var validCompanyIDs []string
for _, companyID := range companyIDs {
_, err = s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Company not found during assignment", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
"company_id": companyID,
})
continue // Skip invalid company IDs
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
// Update customer's company IDs
customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...)
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer with company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to assign companies to customer")
}
s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": validCompanyIDs,
"updated_by": updatedBy,
})
return nil
}
// RemoveCompaniesFromCustomer removes companies from a customer
func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Remove company IDs from customer's list
var updatedCompanyIDs []string
for _, existingID := range customer.CompanyIDs {
shouldRemove := false
for _, removeID := range companyIDs {
if existingID == removeID {
shouldRemove = true
break
}
}
if !shouldRemove {
updatedCompanyIDs = append(updatedCompanyIDs, existingID)
}
}
// Update customer's company IDs
customer.CompanyIDs = updatedCompanyIDs
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer after removing company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to remove companies from customer")
}
s.logger.Info("Companies removed from customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": companyIDs,
"updated_by": updatedBy,
})
return nil
}
// CreateCustomerMobile creates a new customer via mobile app (simplified)
func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) {
// Check if email already exists
@@ -554,10 +787,28 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
return nil, errors.New("customer with this email already exists")
}
// Prepare company assignments
var companyIDs []string
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in mobile form", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
}
companyIDs = form.CompanyIDs
}
// Create customer with mobile form
customer := &Customer{
Type: CustomerType(form.Type),
Status: CustomerStatusActive,
CompanyIDs: companyIDs,
FirstName: form.FirstName,
LastName: form.LastName,
FullName: form.FullName,
@@ -566,7 +817,6 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
Password: "", // Will be set after hashing
Phone: form.Phone,
Mobile: form.Mobile,
CompanyID: form.CompanyID,
Industry: form.Industry,
IsVerified: false,
IsCompliant: false,
@@ -602,6 +852,7 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy,
})
@@ -637,8 +888,22 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f
customer.Mobile = form.Mobile
}
if form.CompanyID != nil {
customer.CompanyID = form.CompanyID
// Handle company assignments
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
var validCompanyIDs []string
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in mobile update", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
customer.CompanyIDs = validCompanyIDs
}
if form.Industry != nil {
@@ -661,6 +926,7 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f
s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"company_ids": customer.CompanyIDs,
"updated_by": *updatedBy,
})
@@ -783,12 +1049,12 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
// Generate JWT tokens using authorization service
companyID := ""
if customer.CompanyID != nil {
companyID = *customer.CompanyID
if len(customer.CompanyIDs) > 0 {
companyID = customer.CompanyIDs[0] // Use first company ID for JWT token
}
tokenPair, err := s.authService.GenerateTokenPair(
customer.ID,
customer.ID.Hex(),
customer.Email,
"customer", // Role for customers
companyID,
@@ -847,7 +1113,7 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
}
// Get customer information
customerID, err := uuid.Parse(validationResult.Claims.UserID)
customerID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
if err != nil {
s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{
"error": err.Error(),
@@ -855,11 +1121,11 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
return nil, errors.New("invalid token format")
}
customer, err := s.repository.GetByID(ctx, customerID.String())
customer, err := s.repository.GetByID(ctx, customerID.Hex())
if err != nil {
s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"customer_id": customerID.Hex(),
})
return nil, errors.New("customer not found")
}
@@ -905,6 +1171,42 @@ func (s *customerService) GetProfile(ctx context.Context, customerID string) (*C
return customer, nil
}
// GetProfileWithCompanies retrieves customer profile information including companies
func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) {
s.logger.Info("Getting customer profile with companies", map[string]interface{}{
"customer_id": customerID,
})
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer profile with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer profile", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{
"customer_id": customerID,
"customer_type": string(customer.Type),
})
return customerResponse, nil
}
// Logout handles customer logout
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
s.logger.Info("Customer logout", map[string]interface{}{
@@ -983,3 +1285,27 @@ func (s *customerService) getDefaultTimezone(timezone *string) string {
}
return "UTC"
}
// loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
var companies []*CompanySummary
// Load companies from CompanyIDs
for _, companyID := range customer.CompanyIDs {
company, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Warn("Failed to load company for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
"company_id": companyID,
})
continue // Skip companies that can't be loaded
}
companies = append(companies, &CompanySummary{
ID: company.ID.Hex(),
Name: company.Name,
})
}
return companies, nil
}