Refactor Customer API Endpoints and Update Documentation

- Changed customer-related API endpoints to improve clarity and consistency, including renaming and restructuring routes.
- Removed unused query parameters and handlers, streamlining the customer management functionality.
- Updated customer entity and forms to reflect new example values for enhanced API documentation clarity.
- Enhanced response structures to return customer entities directly, simplifying the response handling in API endpoints.
- Updated API documentation to reflect changes in endpoint structure and response formats, ensuring consistency for API consumers.
This commit is contained in:
n.nakhostin
2025-09-06 16:08:53 +03:30
parent 05e7b50ec4
commit e848b625ce
9 changed files with 432 additions and 4437 deletions
+42 -146
View File
@@ -13,7 +13,6 @@ const (
CustomerStatusActive CustomerStatus = "active"
CustomerStatusInactive CustomerStatus = "inactive"
CustomerStatusSuspended CustomerStatus = "suspended"
CustomerStatusPending CustomerStatus = "pending"
)
// CustomerType represents the type of customer
@@ -29,52 +28,17 @@ const (
// A customer can have an associated company for login purposes
type Customer struct {
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"`
LastName *string `bson:"last_name,omitempty" json:"last_name,omitempty"`
FullName *string `bson:"full_name,omitempty" json:"full_name,omitempty"`
Username string `bson:"username" json:"username"` // Username for authentication
Email string `bson:"email" json:"email"`
Password string `bson:"password" json:"-"` // Hashed password for authentication
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
Mobile *string `bson:"mobile,omitempty" json:"mobile,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"`
// Address information
Address *Address `bson:"address,omitempty" json:"address,omitempty"`
// Business information
BusinessType *string `bson:"business_type,omitempty" json:"business_type,omitempty"`
EmployeeCount *int `bson:"employee_count,omitempty" json:"employee_count,omitempty"`
AnnualRevenue *float64 `bson:"annual_revenue,omitempty" json:"annual_revenue,omitempty"`
FoundedYear *int `bson:"founded_year,omitempty" json:"founded_year,omitempty"`
// Contact person (for company customers)
ContactPerson *ContactPerson `bson:"contact_person,omitempty" json:"contact_person,omitempty"`
// Verification and compliance
IsVerified bool `bson:"is_verified" json:"is_verified"`
IsCompliant bool `bson:"is_compliant" json:"is_compliant"`
ComplianceNotes *string `bson:"compliance_notes,omitempty" json:"compliance_notes,omitempty"`
// Settings
Language string `bson:"language" json:"language"` // Default: "en"
Currency string `bson:"currency" json:"currency"` // Default: "USD"
Timezone string `bson:"timezone" json:"timezone"` // Default: "UTC"
// Audit fields
CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"`
UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"`
FullName *string `bson:"full_name"`
Username string `bson:"username"`
Email string `bson:"email"`
Password string `bson:"password"`
Status CustomerStatus `bson:"status"`
Type CustomerType `bson:"type"`
Phone *string `bson:"phone"`
CompanyIDs []string `bson:"company_ids"`
LastLoginAt *int64 `bson:"last_login_at"`
UpdatedAt int64 `bson:"updated_at"`
CreatedAt int64 `bson:"created_at"`
}
// SetID sets the customer ID (implements IDSetter interface)
@@ -87,49 +51,26 @@ func (c *Customer) GetID() string {
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (c *Customer) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (c *Customer) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestampable interface)
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *Customer) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface)
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *Customer) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// Address represents a customer's address
type Address struct {
Street string `bson:"street" json:"street"`
City string `bson:"city" json:"city"`
State string `bson:"state" json:"state"`
PostalCode string `bson:"postal_code" json:"postal_code"`
Country string `bson:"country" json:"country"`
AddressType string `bson:"address_type" json:"address_type"` // billing, shipping, etc.
IsDefault bool `bson:"is_default" json:"is_default"`
}
// ContactPerson represents a contact person for company customers
type ContactPerson struct {
FirstName string `bson:"first_name" json:"first_name"`
LastName string `bson:"last_name" json:"last_name"`
FullName string `bson:"full_name" json:"full_name"`
Position string `bson:"position" json:"position"`
Email string `bson:"email" json:"email"`
Phone string `bson:"phone" json:"phone"`
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
IsPrimary bool `bson:"is_primary" json:"is_primary"`
}
// CompanySummary represents company summary data for customer responses
type CompanySummary struct {
ID string `json:"id"`
@@ -138,81 +79,36 @@ type CompanySummary struct {
// 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"`
// Company relationships
Companies []*CompanySummary `json:"companies,omitempty"`
// Company customer fields
RegistrationNumber *string `json:"registration_number,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"`
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"`
UpdatedBy *string `json:"updated_by"`
ID string `bson:"id"`
FullName *string `bson:"full_name"`
Username string `bson:"username"`
Email string `bson:"email"`
Password string `bson:"password"`
Status string `bson:"status"`
Type string `bson:"type"`
Phone *string `bson:"phone"`
CompanyIDs []string `bson:"company_ids"`
UpdatedAt int64 `bson:"updated_at"`
CreatedAt int64 `bson:"created_at"`
LastLoginAt *int64 `bson:"last_login_at"`
Companies []*CompanySummary `json:"companies"`
}
// ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse {
func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse {
return &CustomerResponse{
ID: c.ID.Hex(),
Type: string(c.Type),
Status: string(c.Status),
FirstName: c.FirstName,
LastName: c.LastName,
FullName: c.FullName,
Username: c.Username,
Email: c.Email,
Phone: c.Phone,
Mobile: c.Mobile,
Companies: nil, // Will be populated by service layer
RegistrationNumber: c.RegistrationNumber,
TaxID: c.TaxID,
Industry: c.Industry,
Address: c.Address,
BusinessType: c.BusinessType,
EmployeeCount: c.EmployeeCount,
AnnualRevenue: c.AnnualRevenue,
FoundedYear: c.FoundedYear,
ContactPerson: c.ContactPerson,
IsVerified: c.IsVerified,
IsCompliant: c.IsCompliant,
ComplianceNotes: c.ComplianceNotes,
Language: c.Language,
Currency: c.Currency,
Timezone: c.Timezone,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
CreatedBy: c.CreatedBy,
UpdatedBy: c.UpdatedBy,
ID: c.ID.Hex(),
FullName: c.FullName,
Username: c.Username,
Email: c.Email,
Password: c.Password,
Type: string(c.Type),
Status: string(c.Status),
Phone: c.Phone,
CompanyIDs: c.CompanyIDs,
UpdatedAt: c.UpdatedAt,
CreatedAt: c.CreatedAt,
LastLoginAt: c.LastLoginAt,
Companies: companies,
}
}
// ToResponseWithCompanies converts Customer to CustomerResponse with company information
func (c *Customer) ToResponseWithCompanies(companies []*CompanySummary) *CustomerResponse {
response := c.ToResponse()
response.Companies = companies
return response
}
+31 -173
View File
@@ -1,184 +1,52 @@
package customer
import "tm/pkg/response"
// CreateCustomerForm represents the form for creating a new customer
type CreateCustomerForm struct {
Type string `json:"type" valid:"required,in(individual|company|government)"`
// Company assignments
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"`
Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"`
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
// Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
Email string `json:"email" valid:"required,email"`
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)"`
// Company customer fields
RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"`
TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
// Address information
Address *AddressForm `json:"address,omitempty"`
// Business information
BusinessType *string `json:"business_type,omitempty" valid:"optional,length(2|100)"`
EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"`
AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"`
FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"`
// Contact person (for company customers)
ContactPerson *ContactPersonForm `json:"contact_person,omitempty"`
// Preferences and settings
Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"`
Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"`
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"`
Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"user"`
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
Password string `json:"password" valid:"required,length(8|128)" example:"User!1234"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
}
// UpdateCustomerForm represents the form for updating a customer
type UpdateCustomerForm struct {
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
// Company assignments
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"`
Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"`
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"user"`
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
Password string `json:"password" valid:"required,length(8|128)" example:"User!1234"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
}
// Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
Email *string `json:"email,omitempty" valid:"optional,email"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
// Company customer fields
CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"`
RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"`
TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
// Address information
Address *AddressForm `json:"address,omitempty"`
// Business information
BusinessType *string `json:"business_type,omitempty" valid:"optional,length(2|100)"`
EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"`
AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"`
FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"`
// Contact person (for company customers)
ContactPerson *ContactPersonForm `json:"contact_person,omitempty"`
// Preferences and settings
Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"`
Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"`
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"`
// CustomerStatusForm represents the form for suspending a customer
type UpdateStatusForm struct {
Status string `json:"status" valid:"required,in(active|inactive|suspended)" example:"active"`
Reason string `json:"reason" valid:"required,length(10|500)" example:"Customer is active"`
}
// ListCustomersForm represents the form for listing customers with filters
type ListCustomersForm struct {
Search *string `query:"search" valid:"optional"`
Type *string `query:"type" valid:"optional,in(individual|company|government)"`
Status *string `query:"status" valid:"optional,in(active|inactive|suspended|pending)"`
CompanyID *string `query:"company_id" valid:"optional,uuid"`
Industry *string `query:"industry" valid:"optional"`
IsVerified *bool `query:"is_verified" valid:"optional"`
IsCompliant *bool `query:"is_compliant" valid:"optional"`
Language *string `query:"language" valid:"optional"`
Currency *string `query:"currency" valid:"optional"`
Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,min(0)"`
SortBy *string `query:"sort_by" valid:"optional,in(email|company_name|created_at|updated_at|status)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
}
// UpdateCustomerStatusForm represents the form for updating customer status
type UpdateCustomerStatusForm struct {
Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"`
}
// UpdateCustomerVerificationForm represents the form for updating customer verification status
type UpdateCustomerVerificationForm struct {
IsVerified bool `json:"is_verified"`
IsCompliant bool `json:"is_compliant"`
ComplianceNotes *string `json:"compliance_notes,omitempty" valid:"optional,length(0|1000)"`
}
// SuspendCustomerForm represents the form for suspending a customer
type SuspendCustomerForm struct {
Reason string `json:"reason" valid:"required,length(10|500)"`
}
// AddressForm represents the form for customer address
type AddressForm struct {
Street string `json:"street" valid:"required,length(5|200)"`
City string `json:"city" valid:"required,length(2|100)"`
State string `json:"state" valid:"required,length(2|100)"`
PostalCode string `json:"postal_code" valid:"required,length(3|20)"`
Country string `json:"country" valid:"required,length(2|100)"`
AddressType string `json:"address_type" valid:"required,in(billing|shipping|both)"`
IsDefault bool `json:"is_default"`
}
// ContactPersonForm represents the form for contact person
type ContactPersonForm struct {
FirstName string `json:"first_name" valid:"required,length(2|50)"`
LastName string `json:"last_name" valid:"required,length(2|50)"`
FullName string `json:"full_name" valid:"required,length(2|100)"`
Position string `json:"position" valid:"required,length(2|100)"`
Email string `json:"email" valid:"required,email"`
Phone string `json:"phone" valid:"required,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
IsPrimary bool `json:"is_primary"`
type SearchCustomersForm struct {
Search *string `query:"search" valid:"optional"`
Type *string `query:"type" valid:"optional,in(individual|company|government)"`
Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"`
CompanyID *string `query:"company_id" valid:"optional"`
}
// CustomerListResponse represents the response for listing customers
type CustomerListResponse struct {
Customers []*CustomerResponse `json:"customers"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
TotalPages int `json:"total_pages"`
Customers []*CustomerResponse `json:"customers"`
Meta *response.Meta `json:"meta"`
}
// Mobile-specific forms (simplified for mobile app)
type CreateCustomerMobileForm struct {
Type string `json:"type" valid:"required,in(individual|company)"`
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
Email string `json:"email" valid:"required,email"`
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)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
}
type UpdateCustomerMobileForm struct {
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
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)"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
}
// Customer Authentication DTOs
type LoginForm struct {
Username string `json:"username" valid:"required" example:"nakhostin"`
Password string `json:"password" valid:"required" example:"Nima.1998"`
Username string `json:"username" valid:"required" example:"app"`
Password string `json:"password" valid:"required" example:"App!1234"`
}
type RefreshTokenForm struct {
@@ -191,20 +59,11 @@ type ChangePasswordForm struct {
}
type UpdateProfileForm struct {
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"`
Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"`
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
}
// Customer Authentication Response DTOs
type AuthResponse struct {
Customer *CustomerResponse `json:"customer"`
AccessToken string `json:"access_token"`
@@ -212,7 +71,6 @@ type AuthResponse struct {
ExpiresAt int64 `json:"expires_at"`
}
// Company assignment forms
type AssignCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
+23 -445
View File
@@ -1,7 +1,6 @@
package customer
import (
"strconv"
"tm/internal/user"
"tm/pkg/authorization"
"tm/pkg/logger"
@@ -48,13 +47,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
createdBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
customer, err := h.service.CreateCustomer(c.Request().Context(), form, &createdBy)
customer, err := h.service.Register(c.Request().Context(), form)
if err != nil {
if err.Error() == "customer with this email already exists" ||
err.Error() == "company with this name already exists" ||
@@ -65,7 +58,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to create customer")
}
return response.Created(c, customer.ToResponse(), "Customer created successfully")
return response.Created(c, customer, "Customer created successfully")
}
// GetCustomerByID retrieves a customer by ID (Web Panel)
@@ -82,9 +75,9 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [get]
func (h *Handler) GetCustomerByID(c echo.Context) error {
idStr := c.Param("id")
id := c.Param("id")
customer, err := h.service.GetCustomerByIDWithCompanies(c.Request().Context(), idStr)
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")
@@ -112,19 +105,15 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [put]
func (h *Handler) UpdateCustomer(c echo.Context) error {
idStr := c.Param("id")
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
updatedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
customer, err := h.service.UpdateCustomer(c.Request().Context(), idStr, form, &updatedBy)
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")
@@ -138,7 +127,7 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to update customer")
}
return response.Success(c, customer.ToResponse(), "Customer updated successfully")
return response.Success(c, customer, "Customer updated successfully")
}
// DeleteCustomer deletes a customer (Web Panel)
@@ -155,15 +144,9 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [delete]
func (h *Handler) DeleteCustomer(c echo.Context) error {
idStr := c.Param("id")
id := c.Param("id")
// Get user ID from JWT token context
deletedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.DeleteCustomer(c.Request().Context(), idStr, &deletedBy)
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
@@ -186,11 +169,6 @@ func (h *Handler) DeleteCustomer(c echo.Context) error {
// @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)
@@ -201,13 +179,15 @@ func (h *Handler) DeleteCustomer(c echo.Context) error {
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers [get]
func (h *Handler) ListCustomers(c echo.Context) error {
form, err := response.Parse[ListCustomersForm](c)
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())
}
result, err := h.service.ListCustomers(c.Request().Context(), form)
pagination := response.NewPagination(c)
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
return response.InternalServerError(c, "Failed to list customers")
}
@@ -222,7 +202,7 @@ func (h *Handler) ListCustomers(c echo.Context) error {
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param status body UpdateCustomerStatusForm true "New customer status"
// @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"
@@ -230,24 +210,16 @@ func (h *Handler) ListCustomers(c echo.Context) error {
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/status [patch]
func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[UpdateCustomerStatusForm](c)
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())
}
// Get user ID from JWT token context
updatedBy, err := user.GetUserIDFromContext(c)
err = h.service.UpdateStatus(c.Request().Context(), id, form)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.UpdateCustomerStatus(c.Request().Context(), idStr, form, &updatedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to update customer status")
}
@@ -256,403 +228,9 @@ func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
}, "Customer status updated successfully")
}
// UpdateCustomerVerification updates customer verification status (Web Panel)
// @Summary Update customer verification status
// @Description Update customer verification and compliance status
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param verification body UpdateCustomerVerificationForm true "Verification status update"
// @Success 200 {object} response.APIResponse "Customer verification 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}/verification [patch]
func (h *Handler) UpdateCustomerVerification(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[UpdateCustomerVerificationForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
updatedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.UpdateCustomerVerification(c.Request().Context(), idStr, form, &updatedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to update customer verification")
}
return response.Success(c, map[string]interface{}{
"message": "Customer verification updated successfully",
}, "Customer verification updated successfully")
}
// VerifyCustomer verifies a customer (Web Panel)
// @Summary Verify customer
// @Description Mark a customer as verified
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse "Customer verified 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}/verify [post]
func (h *Handler) VerifyCustomer(c echo.Context) error {
idStr := c.Param("id")
// Get user ID from JWT token context
verifiedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.VerifyCustomer(c.Request().Context(), idStr, &verifiedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to verify customer")
}
return response.Success(c, map[string]interface{}{
"message": "Customer verified successfully",
}, "Customer verified successfully")
}
// SuspendCustomer suspends a customer (Web Panel)
// @Summary Suspend customer
// @Description Suspend a customer account with optional reason
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param suspension body SuspendCustomerForm true "Suspension information"
// @Success 200 {object} response.APIResponse "Customer suspended 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}/suspend [post]
func (h *Handler) SuspendCustomer(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[SuspendCustomerForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
suspendedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.SuspendCustomer(c.Request().Context(), idStr, form.Reason, &suspendedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to suspend customer")
}
return response.Success(c, map[string]interface{}{
"message": "Customer suspended successfully",
}, "Customer suspended successfully")
}
// ActivateCustomer activates a customer (Web Panel)
// @Summary Activate customer
// @Description Activate a suspended customer account
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse "Customer activated 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}/activate [post]
func (h *Handler) ActivateCustomer(c echo.Context) error {
idStr := c.Param("id")
// Get user ID from JWT token context
activatedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.ActivateCustomer(c.Request().Context(), idStr, &activatedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to activate customer")
}
return response.Success(c, map[string]interface{}{
"message": "Customer activated successfully",
}, "Customer activated successfully")
}
// GetCustomersByCompanyID retrieves customers by company ID (Web Panel)
// @Summary Get customers by company ID
// @Description Retrieve all customers associated with a specific company
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param companyId path string true "Company ID"
// @Param limit query int false "Number of customers to return (default: 10, max: 100)"
// @Param offset query int false "Number of customers to skip (default: 0)"
// @Success 200 {object} response.APIResponse{data=[]CustomerResponse} "Customers retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/company/{companyId} [get]
func (h *Handler) GetCustomersByCompanyID(c echo.Context) error {
companyIDStr := c.Param("companyId")
// Parse pagination parameters
limit := 10 // Default limit
offset := 0 // Default offset
if limitStr := c.QueryParam("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
customers, total, err := h.service.GetCustomersByCompanyID(c.Request().Context(), companyIDStr, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to get customers by company")
}
// Convert to responses
var customerResponses []*CustomerResponse
for _, customer := range customers {
customerResponses = append(customerResponses, customer.ToResponse())
}
return response.Success(c, map[string]interface{}{
"customers": customerResponses,
"total": total,
"limit": limit,
"offset": offset,
}, "Customers retrieved successfully")
}
// GetCustomersByType retrieves customers by type
// @Summary Get customers by type
// @Description Retrieve customers filtered by their type (individual, company, or government). Useful for administrative reporting and management.
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param type path string true "Customer type" Enums(individual, company, government)
// @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)
// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer type"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/type/{type} [get]
func (h *Handler) GetCustomersByType(c echo.Context) error {
customerTypeStr := c.Param("type")
customerType := CustomerType(customerTypeStr)
// Validate customer type
if customerType != CustomerTypeIndividual && customerType != CustomerTypeCompany && customerType != CustomerTypeGovernment {
return response.BadRequest(c, "Invalid customer type", "Must be individual, company, or government")
}
limit := 20
if limitStr := c.QueryParam("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
offset := 0
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
customers, total, err := h.service.GetCustomersByType(c.Request().Context(), customerType, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve customers by type")
}
var customerResponses []*CustomerResponse
for _, customer := range customers {
customerResponses = append(customerResponses, customer.ToResponse())
}
meta := &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
}
return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully")
}
// GetCustomersByStatus retrieves customers by status
// @Summary Get customers by status
// @Description Retrieve customers filtered by their account status (active, inactive, suspended, or pending). Useful for administrative monitoring and management.
// @Tags Admin-Customers
// @Accept json
// @Produce json
// @Param status path string true "Customer status" Enums(active, inactive, suspended, pending)
// @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)
// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer status"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/status/{status} [get]
func (h *Handler) GetCustomersByStatus(c echo.Context) error {
statusStr := c.Param("status")
status := CustomerStatus(statusStr)
// Validate customer status
if status != CustomerStatusActive && status != CustomerStatusInactive && status != CustomerStatusSuspended && status != CustomerStatusPending {
return response.BadRequest(c, "Invalid customer status", "Must be active, inactive, suspended, or pending")
}
limit := 20
if limitStr := c.QueryParam("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
offset := 0
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
customers, total, err := h.service.GetCustomersByStatus(c.Request().Context(), status, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve customers by status")
}
var customerResponses []*CustomerResponse
for _, customer := range customers {
customerResponses = append(customerResponses, customer.ToResponse())
}
meta := &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
}
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 Admin-Customers
// @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 Admin-Customers
// @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")
}
// **************************************************
// * PROFILE HANDLERS *
// **************************************************
// Login handles customer authentication
// @Summary Customer login
+93 -285
View File
@@ -5,61 +5,46 @@ import (
"errors"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/bson"
)
// Repository defines the interface for customer data operations
type Repository interface {
Create(ctx context.Context, customer *Customer) error
Login(ctx context.Context, id string) error
Register(ctx context.Context, customer *Customer) error
Update(ctx context.Context, customer *Customer) error
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
GetByID(ctx context.Context, id string) (*Customer, error)
GetByEmail(ctx context.Context, email string) (*Customer, error)
GetByUsername(ctx context.Context, username string) (*Customer, error)
GetByCompanyName(ctx context.Context, companyName string) (*Customer, error)
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)
Update(ctx context.Context, customer *Customer) error
GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error)
Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, 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)
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
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
}
// customerRepository implements the Repository interface using the MongoDB ORM
type customerRepository struct {
ormRepo mongopkg.Repository[Customer]
ormRepo orm.Repository[Customer]
logger logger.Logger
}
func collection() string {
return "customers"
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
func NewCustomerRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
*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_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}}),
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*mongopkg.CreateTextIndex("search_idx", "first_name", "last_name", "full_name", "company_name", "email", "username"),
indexes := []orm.Index{
*orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
*orm.CreateTextIndex("search_idx", "full_name", "email", "username", "phone"),
}
// Create indexes
err := mongoManager.CreateIndexes("customers", indexes)
err := mongoManager.CreateIndexes(collection(), indexes)
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
@@ -67,7 +52,7 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
}
// Create ORM repository
ormRepo := mongopkg.NewRepository[Customer](mongoManager.GetCollection("customers"), logger)
ormRepo := orm.NewRepository[Customer](mongoManager.GetCollection(collection()), logger)
return &customerRepository{
ormRepo: ormRepo,
@@ -75,17 +60,16 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
// Register creates a new customer
func (r *customerRepository) Register(ctx context.Context, customer *Customer) error {
// Set created/updated timestamps using Unix timestamps
now := time.Now().Unix()
customer.SetCreatedAt(now)
customer.SetUpdatedAt(now)
// Use ORM to create customer
err := r.ormRepo.Create(ctx, customer)
if err != nil {
r.logger.Error("Failed to create customer", map[string]interface{}{
r.logger.Error("Failed to register customer", map[string]interface{}{
"error": err.Error(),
"email": customer.Email,
"username": customer.Username,
@@ -93,7 +77,7 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
r.logger.Info("Customer registered successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
@@ -102,11 +86,40 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
return nil
}
// Login updates the last login timestamp
func (r *customerRepository) Login(ctx context.Context, id string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update last login timestamp
customer.LastLoginAt = &[]int64{time.Now().Unix()}[0]
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to update last login", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
r.logger.Info("Last login updated successfully", map[string]interface{}{
"customer_id": id,
})
return nil
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) {
customer, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
@@ -124,7 +137,7 @@ func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Cus
filter := bson.M{"email": email}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by email", map[string]interface{}{
@@ -142,7 +155,7 @@ func (r *customerRepository) GetByUsername(ctx context.Context, username string)
filter := bson.M{"username": username}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by username", map[string]interface{}{
@@ -155,67 +168,13 @@ func (r *customerRepository) GetByUsername(ctx context.Context, username string)
return customer, nil
}
// GetByCompanyName retrieves a customer by company name
func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) {
filter := bson.M{"company_name": companyName}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by company name", map[string]interface{}{
"error": err.Error(),
"company_name": companyName,
})
return nil, err
}
return customer, nil
}
// GetByRegistrationNumber retrieves a customer by registration number
func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) {
filter := bson.M{"registration_number": registrationNumber}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by registration number", map[string]interface{}{
"error": err.Error(),
"registration_number": registrationNumber,
})
return nil, err
}
return customer, nil
}
// GetByTaxID retrieves a customer by tax ID
func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) {
filter := bson.M{"tax_id": taxID}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by tax ID", map[string]interface{}{
"error": err.Error(),
"tax_id": taxID,
})
return nil, err
}
return customer, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) {
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(pagination.SortBy, pagination.SortOrder).
Build()
// Filter by company ID and active status
@@ -225,15 +184,15 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
"limit": limit,
"offset": offset,
"limit": pagination.Limit,
"offset": pagination.Offset,
})
return nil, err
return nil, 0, err
}
// Convert []Customer to []*Customer
@@ -242,7 +201,7 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin
customers[i] = &result.Items[i]
}
return customers, nil
return customers, result.TotalCount, nil
}
// Update updates a customer
@@ -268,20 +227,24 @@ func (r *customerRepository) Update(ctx context.Context, customer *Customer) err
return nil
}
// Delete deletes a customer (soft delete by setting status to inactive)
// Delete deletes a customer (hard delete)
func (r *customerRepository) Delete(ctx context.Context, id string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
_, err := r.GetByID(ctx, id)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return errors.New("customer not found")
}
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
// Update status to inactive
customer.Status = CustomerStatusInactive
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
err = r.ormRepo.Delete(ctx, id)
if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
@@ -297,166 +260,44 @@ func (r *customerRepository) Delete(ctx context.Context, id string) error {
return nil
}
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Only active customers by default
filter := bson.M{"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 list customers", map[string]interface{}{
"error": err.Error(),
"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
}
// Search retrieves customers with search and filters
func (r *customerRepository) 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) {
func (r *customerRepository) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, error) {
// Build filter
filter := bson.M{}
if search != "" {
filter["$text"] = bson.M{"$search": search}
if form.Search != nil {
filter["$text"] = bson.M{"$search": *form.Search}
}
if customerType != nil {
filter["type"] = *customerType
if form.Type != nil {
filter["type"] = *form.Type
}
if status != nil {
filter["status"] = *status
if form.Status != nil {
filter["status"] = *form.Status
}
if companyID != nil {
filter["company_id"] = *companyID
}
if industry != nil {
filter["industry"] = *industry
}
if isVerified != nil {
filter["is_verified"] = *isVerified
}
if isCompliant != nil {
filter["is_compliant"] = *isCompliant
}
if language != nil {
filter["language"] = *language
}
if currency != nil {
filter["currency"] = *currency
if form.CompanyID != nil {
filter["company_id"] = *form.CompanyID
}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset)
// Set sort
if sortBy != "" {
if sortOrder == "desc" {
pagination.SortDesc(sortBy)
} else {
pagination.SortAsc(sortBy)
}
} else {
pagination.SortDesc("created_at") // Default sort
}
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(pagination.SortBy, pagination.SortOrder)
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": search,
"search": form.Search,
})
return nil, err
return nil, 0, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, nil
}
// CountByCompanyID counts customers by company ID
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) {
filter := bson.M{
"company_id": companyID,
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
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{
"type": customerType,
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by type", map[string]interface{}{
"error": err.Error(),
"customer_type": customerType,
})
return 0, err
}
return count, nil
}
// CountByStatus counts customers by status
func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) {
filter := bson.M{"status": status}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return 0, err
}
return count, nil
return result.Items, result.TotalCount, nil
}
// UpdateStatus updates customer status
@@ -489,36 +330,3 @@ func (r *customerRepository) UpdateStatus(ctx context.Context, id string, status
return nil
}
// UpdateVerification updates customer verification status
func (r *customerRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update verification fields
customer.IsVerified = isVerified
customer.IsCompliant = isCompliant
customer.ComplianceNotes = complianceNotes
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to update customer verification", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
r.logger.Info("Customer verification updated successfully", map[string]interface{}{
"customer_id": id,
"is_verified": isVerified,
"is_compliant": isCompliant,
})
return nil
}
+112 -628
View File
@@ -7,6 +7,7 @@ import (
"tm/internal/company"
"tm/pkg/logger"
"tm/pkg/response"
"tm/pkg/authorization"
@@ -17,41 +18,18 @@ import (
// Service defines business logic for customer operations
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)
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)
// Customer management
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)
// Business operations
VerifyCustomer(ctx context.Context, id string, verifiedBy *string) error
SuspendCustomer(ctx context.Context, id string, reason string, suspendedBy *string) error
ActivateCustomer(ctx context.Context, id string, activatedBy *string) error
Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error)
GetByID(ctx context.Context, id string) (*CustomerResponse, error)
Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error)
Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error)
Delete(ctx context.Context, id string) error
UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error
// Authentication operations
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
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
Logout(ctx context.Context, id string, accessToken string) error
}
// customerService implements the Service interface
@@ -72,34 +50,17 @@ func NewCustomerService(repository Repository, logger logger.Logger, authService
}
}
// CreateCustomer creates a new customer
func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) {
// Register creates a new customer
func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
return nil, errors.New("customer with this email already exists")
}
// Check if registration number already exists (for company customers)
if form.Type == string(CustomerTypeCompany) && form.RegistrationNumber != nil {
existingCustomer, _ = s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber)
if existingCustomer != nil {
return nil, errors.New("company with this registration number already exists")
}
}
// Check if tax ID already exists (for company customers)
if form.Type == string(CustomerTypeCompany) && form.TaxID != nil {
existingCustomer, _ = s.repository.GetByTaxID(ctx, *form.TaxID)
if existingCustomer != nil {
return nil, errors.New("company with this tax ID already exists")
}
}
// Prepare company assignments
// Verify that all company IDs exist
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 {
@@ -124,36 +85,18 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
// Create customer
customer := &Customer{
Type: CustomerType(form.Type),
Status: CustomerStatusPending,
CompanyIDs: companyIDs,
FirstName: form.FirstName,
LastName: form.LastName,
FullName: form.FullName,
Username: form.Username,
Email: form.Email,
Password: string(hashedPassword),
Phone: form.Phone,
Mobile: form.Mobile,
RegistrationNumber: form.RegistrationNumber,
TaxID: form.TaxID,
Industry: form.Industry,
Address: s.convertAddressForm(form.Address),
BusinessType: form.BusinessType,
EmployeeCount: form.EmployeeCount,
AnnualRevenue: form.AnnualRevenue,
FoundedYear: form.FoundedYear,
ContactPerson: s.convertContactPersonForm(form.ContactPerson),
IsVerified: false,
IsCompliant: false,
Language: s.getDefaultLanguage(form.Language),
Currency: s.getDefaultCurrency(form.Currency),
Timezone: s.getDefaultTimezone(form.Timezone),
CreatedBy: createdBy,
Type: CustomerType(form.Type),
Status: CustomerStatusActive,
CompanyIDs: companyIDs,
FullName: form.FullName,
Username: form.Username,
Email: form.Email,
Password: string(hashedPassword),
Phone: form.Phone,
}
// Save to database
err = s.repository.Create(ctx, customer)
err = s.repository.Register(ctx, customer)
if err != nil {
s.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
@@ -168,14 +111,13 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
"email": customer.Email,
"type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy,
})
return customer, nil
return customer.ToResponse(nil), nil
}
// GetCustomerByID retrieves a customer by ID
func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Customer, error) {
// GetByID retrieves a customer by ID
func (s *customerService) GetByID(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", map[string]interface{}{
@@ -185,48 +127,56 @@ func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Cust
return nil, err
}
var companies []*CompanySummary
if len(customer.CompanyIDs) > 0 {
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,
})
}
}
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return customer, nil
return customer.ToResponse(companies), 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)
// Search searches for customers
func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) {
customers, total, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to get customer by ID with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
s.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
})
return nil, err
return nil, errors.New("failed to search customers")
}
// 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{}
var customerResponses []*CustomerResponse
for _, customer := range customers {
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,
})
}
customerResponses = append(customerResponses, customer.ToResponse(companies))
}
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
return &CustomerListResponse{
Customers: customerResponses,
Meta: pagination.Response(total),
}, nil
}
// UpdateCustomer updates a customer
func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) {
// Update updates a customer
func (s *customerService) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) {
// Get existing customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -238,44 +188,21 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
}
// Check if email already exists (if changing email)
if form.Email != nil && *form.Email != customer.Email {
existingCustomer, _ := s.repository.GetByEmail(ctx, *form.Email)
if form.Email != "" && form.Email != customer.Email {
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
return nil, errors.New("customer with this email already exists")
}
customer.Email = *form.Email
customer.Email = form.Email
}
// Check if username already exists (if changing username)
if form.Username != nil && *form.Username != customer.Username {
existingCustomer, _ := s.repository.GetByUsername(ctx, *form.Username)
if form.Username != "" && form.Username != customer.Username {
existingCustomer, _ := s.repository.GetByUsername(ctx, form.Username)
if existingCustomer != nil {
return nil, errors.New("customer with this username already exists")
}
customer.Username = *form.Username
}
// Check if registration number already exists (if changing)
if form.RegistrationNumber != nil && *form.RegistrationNumber != *customer.RegistrationNumber {
existingCustomer, _ := s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber)
if existingCustomer != nil {
return nil, errors.New("company with this registration number already exists")
}
customer.RegistrationNumber = form.RegistrationNumber
}
// Check if tax ID already exists (if changing)
if form.TaxID != nil && *form.TaxID != *customer.TaxID {
existingCustomer, _ := s.repository.GetByTaxID(ctx, *form.TaxID)
if existingCustomer != nil {
return nil, errors.New("company with this tax ID already exists")
}
customer.TaxID = form.TaxID
}
// Update fields if provided
if form.Type != nil {
customer.Type = CustomerType(*form.Type)
customer.Username = form.Username
}
// Handle company assignments
@@ -296,14 +223,6 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.CompanyIDs = validCompanyIDs
}
if form.FirstName != nil {
customer.FirstName = form.FirstName
}
if form.LastName != nil {
customer.LastName = form.LastName
}
if form.FullName != nil {
customer.FullName = form.FullName
}
@@ -312,54 +231,6 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.Phone = form.Phone
}
if form.Mobile != nil {
customer.Mobile = form.Mobile
}
if form.Industry != nil {
customer.Industry = form.Industry
}
if form.Address != nil {
customer.Address = s.convertAddressForm(form.Address)
}
if form.BusinessType != nil {
customer.BusinessType = form.BusinessType
}
if form.EmployeeCount != nil {
customer.EmployeeCount = form.EmployeeCount
}
if form.AnnualRevenue != nil {
customer.AnnualRevenue = form.AnnualRevenue
}
if form.FoundedYear != nil {
customer.FoundedYear = form.FoundedYear
}
if form.ContactPerson != nil {
customer.ContactPerson = s.convertContactPersonForm(form.ContactPerson)
}
if form.Language != nil {
customer.Language = *form.Language
}
if form.Currency != nil {
customer.Currency = *form.Currency
}
if form.Timezone != nil {
customer.Timezone = *form.Timezone
}
// Set updated by
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
@@ -373,23 +244,23 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
s.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"updated_by": *updatedBy,
})
return customer, nil
return customer.ToResponse(nil), nil
}
// DeleteCustomer deletes a customer (soft delete)
func (s *customerService) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error {
// Delete deletes a customer (soft delete)
func (s *customerService) Delete(ctx context.Context, id string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, id)
_, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer for delete", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("customer not found")
}
// Set updated by before deletion
customer.UpdatedBy = deletedBy
// Delete customer (soft delete)
err = s.repository.Delete(ctx, id)
if err != nil {
@@ -402,154 +273,13 @@ func (s *customerService) DeleteCustomer(ctx context.Context, id string, deleted
s.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id,
"deleted_by": deletedBy,
})
return nil
}
// ListCustomers lists customers with search and filters
func (s *customerService) ListCustomers(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", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to list customers")
}
// Convert to responses
var customerResponses []*CustomerResponse
for _, customer := range customers {
customerResponses = append(customerResponses, customer.ToResponse())
}
// 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)
if err != nil {
s.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, 0, errors.New("failed to get customers by company")
}
// Get total count
total, err := s.repository.CountByCompanyID(ctx, companyID)
if err != nil {
s.logger.Error("Failed to count customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return customers, 0, nil // Return customers even if count fails
}
return customers, total, nil
}
// GetCustomersByType retrieves customers by type with pagination
func (s *customerService) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) {
// This would need to be implemented in the repository
// For now, we'll use the search method
customerTypeStr := string(customerType)
customers, err := s.repository.Search(ctx, "", &customerTypeStr, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc")
if err != nil {
s.logger.Error("Failed to get customers by type", map[string]interface{}{
"error": err.Error(),
"customer_type": customerType,
})
return nil, 0, errors.New("failed to get customers by type")
}
// Get total count
total, err := s.repository.CountByType(ctx, customerType)
if err != nil {
s.logger.Error("Failed to count customers by type", map[string]interface{}{
"error": err.Error(),
"customer_type": customerType,
})
return customers, 0, nil
}
return customers, total, nil
}
// GetCustomersByStatus retrieves customers by status with pagination
func (s *customerService) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) {
// This would need to be implemented in the repository
// For now, we'll use the search method
statusStr := string(status)
customers, err := s.repository.Search(ctx, "", nil, &statusStr, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc")
if err != nil {
s.logger.Error("Failed to get customers by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return nil, 0, errors.New("failed to get customers by status")
}
// Get total count
total, err := s.repository.CountByStatus(ctx, status)
if err != nil {
s.logger.Error("Failed to count customers by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return customers, 0, nil
}
return customers, total, nil
}
// UpdateCustomerStatus updates customer status
func (s *customerService) UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error {
// UpdateStatus updates customer status
func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -571,42 +301,13 @@ func (s *customerService) UpdateCustomerStatus(ctx context.Context, id string, f
s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id,
"status": form.Status,
"updated_by": updatedBy,
})
return nil
}
// UpdateCustomerVerification updates customer verification status
func (s *customerService) UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update verification
err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes)
if err != nil {
s.logger.Error("Failed to update customer verification", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("failed to update customer verification")
}
s.logger.Info("Customer verification updated successfully", map[string]interface{}{
"customer_id": id,
"is_verified": form.IsVerified,
"is_compliant": form.IsCompliant,
"updated_by": updatedBy,
})
return nil
}
// AssignCompaniesToCustomer assigns companies to a customer
func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error {
// AssignCompanies assigns companies to a customer
func (s *customerService) AssignCompanies(ctx context.Context, customerID string, companyIDs []string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
@@ -630,7 +331,6 @@ func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, custome
// 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)
@@ -645,14 +345,13 @@ func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, custome
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 {
func (s *customerService) RemoveCompanies(ctx context.Context, customerID string, companyIDs []string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
@@ -676,7 +375,6 @@ func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, custo
// Update customer's company IDs
customer.CompanyIDs = updatedCompanyIDs
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
@@ -691,194 +389,13 @@ func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, custo
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
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
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,
Username: form.Username,
Email: form.Email,
Password: "", // Will be set after hashing
Phone: form.Phone,
Mobile: form.Mobile,
Industry: form.Industry,
IsVerified: false,
IsCompliant: false,
Language: "en",
Currency: "USD",
Timezone: "UTC",
CreatedBy: createdBy,
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash password for mobile customer", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to process password")
}
customer.Password = string(hashedPassword)
// Save to database
err = s.repository.Create(ctx, customer)
if err != nil {
s.logger.Error("Failed to create customer via mobile", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
"type": form.Type,
"created_by": createdBy,
})
return nil, err
}
s.logger.Info("Customer created via mobile successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy,
})
return customer, nil
}
// UpdateCustomerMobile updates a customer via mobile app (simplified)
func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error) {
// Get existing customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return nil, errors.New("customer not found")
}
// Update fields if provided
if form.FirstName != nil {
customer.FirstName = form.FirstName
}
if form.LastName != nil {
customer.LastName = form.LastName
}
if form.FullName != nil {
customer.FullName = form.FullName
}
if form.Phone != nil {
customer.Phone = form.Phone
}
if form.Mobile != nil {
customer.Mobile = form.Mobile
}
// 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 {
customer.Industry = form.Industry
}
// Set updated by
customer.UpdatedBy = updatedBy
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer via mobile", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, errors.New("failed to update customer")
}
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,
})
return customer, nil
}
// VerifyCustomer verifies a customer
func (s *customerService) VerifyCustomer(ctx context.Context, id string, verifiedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return errors.New("customer not found")
}
// Update verification
err = s.repository.UpdateVerification(ctx, id, true, customer.IsCompliant, customer.ComplianceNotes)
if err != nil {
s.logger.Error("Failed to verify customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return errors.New("failed to verify customer")
}
s.logger.Info("Customer verified successfully", map[string]interface{}{
"customer_id": id,
"verified_by": verifiedBy,
})
return nil
}
// SuspendCustomer suspends a customer
func (s *customerService) SuspendCustomer(ctx context.Context, id string, reason string, suspendedBy *string) error {
// Suspend suspends a customer
func (s *customerService) Suspend(ctx context.Context, id string, reason string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -896,16 +413,15 @@ func (s *customerService) SuspendCustomer(ctx context.Context, id string, reason
}
s.logger.Info("Customer suspended successfully", map[string]interface{}{
"customer_id": id,
"reason": reason,
"suspended_by": suspendedBy,
"customer_id": id,
"reason": reason,
})
return nil
}
// ActivateCustomer activates a customer
func (s *customerService) ActivateCustomer(ctx context.Context, id string, activatedBy *string) error {
// Activate activates a customer
func (s *customerService) Activate(ctx context.Context, id string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -923,8 +439,7 @@ func (s *customerService) ActivateCustomer(ctx context.Context, id string, activ
}
s.logger.Info("Customer activated successfully", map[string]interface{}{
"customer_id": id,
"activated_by": activatedBy,
"customer_id": id,
})
return nil
@@ -985,6 +500,22 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
return nil, errors.New("failed to generate authentication tokens")
}
err = s.repository.Login(ctx, customer.ID.Hex())
if err != nil {
s.logger.Error("Failed to update customer last login", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
}
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,
})
}
s.logger.Info("Customer login successful", map[string]interface{}{
"username": form.Username,
"customer_id": customer.ID,
@@ -992,7 +523,7 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
})
return &AuthResponse{
Customer: customer.ToResponse(),
Customer: customer.ToResponse(companies),
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
@@ -1053,13 +584,21 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
return nil, errors.New("customer account is inactive")
}
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,
})
}
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
"customer_id": customer.ID,
"customer_type": string(customer.Type),
})
return &AuthResponse{
Customer: customer.ToResponse(),
Customer: customer.ToResponse(companies),
AccessToken: newAccessToken,
RefreshToken: refreshToken, // Return the same refresh token
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
@@ -1115,7 +654,7 @@ func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerI
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
customerResponse := customer.ToResponse(companies)
s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{
"customer_id": customerID,
@@ -1149,61 +688,6 @@ func (s *customerService) Logout(ctx context.Context, customerID string, accessT
return nil
}
// Helper methods for converting forms to domain objects
func (s *customerService) convertAddressForm(form *AddressForm) *Address {
if form == nil {
return nil
}
return &Address{
Street: form.Street,
City: form.City,
State: form.State,
PostalCode: form.PostalCode,
Country: form.Country,
AddressType: form.AddressType,
IsDefault: form.IsDefault,
}
}
func (s *customerService) convertContactPersonForm(form *ContactPersonForm) *ContactPerson {
if form == nil {
return nil
}
return &ContactPerson{
FirstName: form.FirstName,
LastName: form.LastName,
FullName: form.FullName,
Position: form.Position,
Email: form.Email,
Phone: form.Phone,
Mobile: form.Mobile,
IsPrimary: form.IsPrimary,
}
}
func (s *customerService) getDefaultLanguage(language *string) string {
if language != nil {
return *language
}
return "en"
}
func (s *customerService) getDefaultCurrency(currency *string) string {
if currency != nil {
return *currency
}
return "USD"
}
func (s *customerService) getDefaultTimezone(timezone *string) string {
if timezone != nil {
return *timezone
}
return "UTC"
}
// loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
if len(customer.CompanyIDs) == 0 {