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
+48 -1040
View File
File diff suppressed because it is too large Load Diff
+48 -1040
View File
File diff suppressed because it is too large Load Diff
+33 -669
View File
@@ -435,48 +435,6 @@ definitions:
is_verified: is_verified:
type: boolean type: boolean
type: object type: object
customer.Address:
properties:
address_type:
description: billing, shipping, etc.
type: string
city:
type: string
country:
type: string
is_default:
type: boolean
postal_code:
type: string
state:
type: string
street:
type: string
type: object
customer.AddressForm:
properties:
address_type:
type: string
city:
type: string
country:
type: string
is_default:
type: boolean
postal_code:
type: string
state:
type: string
street:
type: string
type: object
customer.AssignCompaniesForm:
properties:
company_ids:
items:
type: string
type: array
type: object
customer.AuthResponse: customer.AuthResponse:
properties: properties:
access_token: access_token:
@@ -495,100 +453,29 @@ definitions:
name: name:
type: string type: string
type: object type: object
customer.ContactPerson:
properties:
email:
type: string
first_name:
type: string
full_name:
type: string
is_primary:
type: boolean
last_name:
type: string
mobile:
type: string
phone:
type: string
position:
type: string
type: object
customer.ContactPersonForm:
properties:
email:
type: string
first_name:
type: string
full_name:
type: string
is_primary:
type: boolean
last_name:
type: string
mobile:
type: string
phone:
type: string
position:
type: string
type: object
customer.CreateCustomerForm: customer.CreateCustomerForm:
properties: properties:
address:
allOf:
- $ref: '#/definitions/customer.AddressForm'
description: Address information
annual_revenue:
type: number
business_type:
description: Business information
type: string
company_ids: company_ids:
description: Company assignments
items: items:
type: string type: string
type: array type: array
contact_person:
allOf:
- $ref: '#/definitions/customer.ContactPersonForm'
description: Contact person (for company customers)
currency:
type: string
email: email:
example: user@opplens.com
type: string type: string
employee_count:
type: integer
first_name:
description: Individual customer fields
type: string
founded_year:
type: integer
full_name: full_name:
type: string example: User
industry:
type: string
language:
description: Preferences and settings
type: string
last_name:
type: string
mobile:
type: string type: string
password: password:
example: User!1234
type: string type: string
phone: phone:
type: string example: "+1234567890"
registration_number:
description: Company customer fields
type: string
tax_id:
type: string
timezone:
type: string type: string
type: type:
example: individual
type: string type: string
username: username:
example: user
type: string type: string
type: object type: object
customer.CustomerListResponse: customer.CustomerListResponse:
@@ -597,89 +484,49 @@ definitions:
items: items:
$ref: '#/definitions/customer.CustomerResponse' $ref: '#/definitions/customer.CustomerResponse'
type: array type: array
limit: meta:
type: integer $ref: '#/definitions/response.Meta'
offset:
type: integer
total:
type: integer
total_pages:
type: integer
type: object type: object
customer.CustomerResponse: customer.CustomerResponse:
properties: properties:
address:
$ref: '#/definitions/customer.Address'
annual_revenue:
type: number
business_type:
type: string
companies: companies:
description: Company relationships
items: items:
$ref: '#/definitions/customer.CompanySummary' $ref: '#/definitions/customer.CompanySummary'
type: array type: array
compliance_notes: companyIDs:
type: string items:
contact_person: type: string
$ref: '#/definitions/customer.ContactPerson' type: array
created_at: createdAt:
type: integer type: integer
created_by:
type: string
currency:
type: string
email: email:
type: string type: string
employee_count: fullName:
type: integer
first_name:
type: string
founded_year:
type: integer
full_name:
type: string type: string
id: id:
type: string type: string
industry: lastLoginAt:
type: string type: integer
is_compliant: password:
type: boolean
is_verified:
type: boolean
language:
type: string
last_name:
type: string
mobile:
type: string type: string
phone: phone:
type: string type: string
registration_number:
description: Company customer fields
type: string
status: status:
type: string type: string
tax_id:
type: string
timezone:
type: string
type: type:
type: string type: string
updated_at: updatedAt:
type: integer type: integer
updated_by:
type: string
username: username:
type: string type: string
type: object type: object
customer.LoginForm: customer.LoginForm:
properties: properties:
password: password:
example: Nima.1998 example: App!1234
type: string type: string
username: username:
example: nakhostin example: app
type: string type: string
type: object type: object
customer.RefreshTokenForm: customer.RefreshTokenForm:
@@ -687,90 +534,40 @@ definitions:
refresh_token: refresh_token:
type: string type: string
type: object type: object
customer.RemoveCompaniesForm:
properties:
company_ids:
items:
type: string
type: array
type: object
customer.SuspendCustomerForm:
properties:
reason:
type: string
type: object
customer.UpdateCustomerForm: customer.UpdateCustomerForm:
properties: properties:
address:
allOf:
- $ref: '#/definitions/customer.AddressForm'
description: Address information
annual_revenue:
type: number
business_type:
description: Business information
type: string
company_ids: company_ids:
description: Company assignments
items: items:
type: string type: string
type: array type: array
company_name:
description: Company customer fields
type: string
contact_person:
allOf:
- $ref: '#/definitions/customer.ContactPersonForm'
description: Contact person (for company customers)
currency:
type: string
email: email:
example: user@opplens.com
type: string type: string
employee_count:
type: integer
first_name:
description: Individual customer fields
type: string
founded_year:
type: integer
full_name: full_name:
example: User
type: string type: string
industry: password:
type: string example: User!1234
language:
description: Preferences and settings
type: string
last_name:
type: string
mobile:
type: string type: string
phone: phone:
type: string example: "+1234567890"
registration_number:
type: string
tax_id:
type: string
timezone:
type: string type: string
type: type:
example: individual
type: string type: string
username: username:
example: user
type: string type: string
type: object type: object
customer.UpdateCustomerStatusForm: customer.UpdateStatusForm:
properties: properties:
reason:
example: Customer is active
type: string
status: status:
example: active
type: string type: string
type: object type: object
customer.UpdateCustomerVerificationForm:
properties:
compliance_notes:
type: string
is_compliant:
type: boolean
is_verified:
type: boolean
type: object
feedback.CompanyFeedbackStatsResponse: feedback.CompanyFeedbackStatsResponse:
properties: properties:
company_id: company_id:
@@ -2477,26 +2274,6 @@ paths:
in: query in: query
name: company_id name: company_id
type: string type: string
- description: Filter by industry
in: query
name: industry
type: string
- description: Filter by verification status
in: query
name: is_verified
type: boolean
- description: Filter by compliance status
in: query
name: is_compliant
type: boolean
- description: Filter by preferred language
in: query
name: language
type: string
- description: Filter by preferred currency
in: query
name: currency
type: string
- default: 20 - default: 20
description: Number of customers per page (1-100) description: Number of customers per page (1-100)
in: query in: query
@@ -2733,131 +2510,6 @@ paths:
summary: Update customer information summary: Update customer information
tags: tags:
- Admin-Customers - Admin-Customers
/admin/v1/customers/{id}/activate:
post:
consumes:
- application/json
description: Activate a suspended customer account
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Customer activated successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid customer ID
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Activate customer
tags:
- Admin-Customers
/admin/v1/customers/{id}/companies/assign:
post:
consumes:
- application/json
description: Assign one or more companies to a specific customer.
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
- description: List of company IDs to assign
in: body
name: companies
required: true
schema:
$ref: '#/definitions/customer.AssignCompaniesForm'
produces:
- application/json
responses:
"200":
description: Companies assigned successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Assign companies to a customer
tags:
- Admin-Customers
/admin/v1/customers/{id}/companies/remove:
post:
consumes:
- application/json
description: Remove one or more companies from a specific customer.
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
- description: List of company IDs to remove
in: body
name: companies
required: true
schema:
$ref: '#/definitions/customer.RemoveCompaniesForm'
produces:
- application/json
responses:
"200":
description: Companies removed successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Remove companies from a customer
tags:
- Admin-Customers
/admin/v1/customers/{id}/status: /admin/v1/customers/{id}/status:
patch: patch:
consumes: consumes:
@@ -2874,7 +2526,7 @@ paths:
name: status name: status
required: true required: true
schema: schema:
$ref: '#/definitions/customer.UpdateCustomerStatusForm' $ref: '#/definitions/customer.UpdateStatusForm'
produces: produces:
- application/json - application/json
responses: responses:
@@ -2903,294 +2555,6 @@ paths:
summary: Update customer status summary: Update customer status
tags: tags:
- Admin-Customers - Admin-Customers
/admin/v1/customers/{id}/suspend:
post:
consumes:
- application/json
description: Suspend a customer account with optional reason
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
- description: Suspension information
in: body
name: suspension
required: true
schema:
$ref: '#/definitions/customer.SuspendCustomerForm'
produces:
- application/json
responses:
"200":
description: Customer suspended successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Suspend customer
tags:
- Admin-Customers
/admin/v1/customers/{id}/verification:
patch:
consumes:
- application/json
description: Update customer verification and compliance status
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
- description: Verification status update
in: body
name: verification
required: true
schema:
$ref: '#/definitions/customer.UpdateCustomerVerificationForm'
produces:
- application/json
responses:
"200":
description: Customer verification updated successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update customer verification status
tags:
- Admin-Customers
/admin/v1/customers/{id}/verify:
post:
consumes:
- application/json
description: Mark a customer as verified
parameters:
- description: Customer ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Customer verified successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid customer ID
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Customer not found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Verify customer
tags:
- Admin-Customers
/admin/v1/customers/company/{companyId}:
get:
consumes:
- application/json
description: Retrieve all customers associated with a specific company
parameters:
- description: Company ID
in: path
name: companyId
required: true
type: string
- description: 'Number of customers to return (default: 10, max: 100)'
in: query
name: limit
type: integer
- description: 'Number of customers to skip (default: 0)'
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: Customers retrieved successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
items:
$ref: '#/definitions/customer.CustomerResponse'
type: array
type: object
"400":
description: Bad request - Invalid company ID
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get customers by company ID
tags:
- Admin-Customers
/admin/v1/customers/status/{status}:
get:
consumes:
- application/json
description: Retrieve customers filtered by their account status (active, inactive,
suspended, or pending). Useful for administrative monitoring and management.
parameters:
- description: Customer status
enum:
- active
- inactive
- suspended
- pending
in: path
name: status
required: true
type: string
- default: 20
description: Number of customers per page (1-100)
in: query
maximum: 100
minimum: 1
name: limit
type: integer
- default: 0
description: Number of customers to skip for pagination
in: query
minimum: 0
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: Customers retrieved successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
items:
$ref: '#/definitions/customer.CustomerResponse'
type: array
meta:
$ref: '#/definitions/response.Meta'
type: object
"400":
description: Bad request - Invalid customer status
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get customers by status
tags:
- Admin-Customers
/admin/v1/customers/type/{type}:
get:
consumes:
- application/json
description: Retrieve customers filtered by their type (individual, company,
or government). Useful for administrative reporting and management.
parameters:
- description: Customer type
enum:
- individual
- company
- government
in: path
name: type
required: true
type: string
- default: 20
description: Number of customers per page (1-100)
in: query
maximum: 100
minimum: 1
name: limit
type: integer
- default: 0
description: Number of customers to skip for pagination
in: query
minimum: 0
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: Customers retrieved successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
items:
$ref: '#/definitions/customer.CustomerResponse'
type: array
meta:
$ref: '#/definitions/response.Meta'
type: object
"400":
description: Bad request - Invalid customer type
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get customers by type
tags:
- Admin-Customers
/admin/v1/feedback: /admin/v1/feedback:
get: get:
consumes: consumes:
+2 -11
View File
@@ -71,20 +71,11 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
{ {
customersGP.Use(userHandler.AuthMiddleware()) customersGP.Use(userHandler.AuthMiddleware())
customersGP.POST("", customerHandler.CreateCustomer) customersGP.POST("", customerHandler.CreateCustomer)
customersGP.GET("", customerHandler.ListCustomers) customersGP.GET("", customerHandler.Search)
customersGP.GET("/:id", customerHandler.GetCustomerByID) customersGP.GET("/:id", customerHandler.GetCustomerByID)
customersGP.PUT("/:id", customerHandler.UpdateCustomer) customersGP.PUT("/:id", customerHandler.UpdateCustomer)
customersGP.DELETE("/:id", customerHandler.DeleteCustomer) customersGP.DELETE("/:id", customerHandler.DeleteCustomer)
customersGP.PATCH("/:id/status", customerHandler.UpdateCustomerStatus) customersGP.PUT("/:id/status", customerHandler.UpdateStatus)
customersGP.PATCH("/:id/verification", customerHandler.UpdateCustomerVerification)
customersGP.POST("/:id/verify", customerHandler.VerifyCustomer)
customersGP.POST("/:id/suspend", customerHandler.SuspendCustomer)
customersGP.POST("/:id/activate", customerHandler.ActivateCustomer)
customersGP.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID)
customersGP.GET("/type/:type", customerHandler.GetCustomersByType)
customersGP.GET("/status/:status", customerHandler.GetCustomersByStatus)
customersGP.POST("/:id/companies/assign", customerHandler.AssignCompaniesToCustomer)
customersGP.POST("/:id/companies/remove", customerHandler.RemoveCompaniesFromCustomer)
} }
// Admin Tenders Routes // Admin Tenders Routes
+42 -146
View File
@@ -13,7 +13,6 @@ const (
CustomerStatusActive CustomerStatus = "active" CustomerStatusActive CustomerStatus = "active"
CustomerStatusInactive CustomerStatus = "inactive" CustomerStatusInactive CustomerStatus = "inactive"
CustomerStatusSuspended CustomerStatus = "suspended" CustomerStatusSuspended CustomerStatus = "suspended"
CustomerStatusPending CustomerStatus = "pending"
) )
// CustomerType represents the type of customer // CustomerType represents the type of customer
@@ -29,52 +28,17 @@ const (
// A customer can have an associated company for login purposes // A customer can have an associated company for login purposes
type Customer struct { type Customer struct {
mongo.Model `bson:",inline"` mongo.Model `bson:",inline"`
Type CustomerType `bson:"type" json:"type"` FullName *string `bson:"full_name"`
Status CustomerStatus `bson:"status" json:"status"` Username string `bson:"username"`
Email string `bson:"email"`
// Individual customer fields Password string `bson:"password"`
FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"` Status CustomerStatus `bson:"status"`
LastName *string `bson:"last_name,omitempty" json:"last_name,omitempty"` Type CustomerType `bson:"type"`
FullName *string `bson:"full_name,omitempty" json:"full_name,omitempty"` Phone *string `bson:"phone"`
Username string `bson:"username" json:"username"` // Username for authentication CompanyIDs []string `bson:"company_ids"`
Email string `bson:"email" json:"email"` LastLoginAt *int64 `bson:"last_login_at"`
Password string `bson:"password" json:"-"` // Hashed password for authentication UpdatedAt int64 `bson:"updated_at"`
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` CreatedAt int64 `bson:"created_at"`
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"`
} }
// SetID sets the customer ID (implements IDSetter interface) // SetID sets the customer ID (implements IDSetter interface)
@@ -87,49 +51,26 @@ func (c *Customer) GetID() string {
return c.ID.Hex() 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) { func (c *Customer) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp 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) { func (c *Customer) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp c.UpdatedAt = timestamp
} }
// GetCreatedAt returns the created timestamp (implements Timestampable interface) // GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *Customer) GetCreatedAt() int64 { func (c *Customer) GetCreatedAt() int64 {
return c.CreatedAt return c.CreatedAt
} }
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface) // GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *Customer) GetUpdatedAt() int64 { func (c *Customer) GetUpdatedAt() int64 {
return c.UpdatedAt 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 // CompanySummary represents company summary data for customer responses
type CompanySummary struct { type CompanySummary struct {
ID string `json:"id"` ID string `json:"id"`
@@ -138,81 +79,36 @@ type CompanySummary struct {
// CustomerResponse represents the customer data sent in API responses // CustomerResponse represents the customer data sent in API responses
type CustomerResponse struct { type CustomerResponse struct {
ID string `json:"id"` ID string `bson:"id"`
Type string `json:"type"` FullName *string `bson:"full_name"`
Status string `json:"status"` Username string `bson:"username"`
FirstName *string `json:"first_name,omitempty"` Email string `bson:"email"`
LastName *string `json:"last_name,omitempty"` Password string `bson:"password"`
FullName *string `json:"full_name,omitempty"` Status string `bson:"status"`
Email string `json:"email"` Type string `bson:"type"`
Phone *string `json:"phone,omitempty"` Phone *string `bson:"phone"`
Mobile *string `json:"mobile,omitempty"` CompanyIDs []string `bson:"company_ids"`
UpdatedAt int64 `bson:"updated_at"`
// Company relationships CreatedAt int64 `bson:"created_at"`
Companies []*CompanySummary `json:"companies,omitempty"` LastLoginAt *int64 `bson:"last_login_at"`
Companies []*CompanySummary `json:"companies"`
// 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"`
} }
// ToResponse converts Customer to CustomerResponse // ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse { func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse {
return &CustomerResponse{ return &CustomerResponse{
ID: c.ID.Hex(), ID: c.ID.Hex(),
Type: string(c.Type), FullName: c.FullName,
Status: string(c.Status), Username: c.Username,
FirstName: c.FirstName, Email: c.Email,
LastName: c.LastName, Password: c.Password,
FullName: c.FullName, Type: string(c.Type),
Username: c.Username, Status: string(c.Status),
Email: c.Email, Phone: c.Phone,
Phone: c.Phone, CompanyIDs: c.CompanyIDs,
Mobile: c.Mobile, UpdatedAt: c.UpdatedAt,
Companies: nil, // Will be populated by service layer CreatedAt: c.CreatedAt,
RegistrationNumber: c.RegistrationNumber, LastLoginAt: c.LastLoginAt,
TaxID: c.TaxID, Companies: companies,
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,
} }
} }
// 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 package customer
import "tm/pkg/response"
// CreateCustomerForm represents the form for creating a new customer // CreateCustomerForm represents the form for creating a new customer
type CreateCustomerForm struct { type CreateCustomerForm struct {
Type string `json:"type" valid:"required,in(individual|company|government)"` 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"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"user"`
// Individual customer fields Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` Password string `json:"password" valid:"required,length(8|128)" example:"User!1234"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
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)"`
} }
// UpdateCustomerForm represents the form for updating a customer // UpdateCustomerForm represents the form for updating a customer
type UpdateCustomerForm struct { type UpdateCustomerForm struct {
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"` 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"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` 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 // CustomerStatusForm represents the form for suspending a customer
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` type UpdateStatusForm struct {
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` Status string `json:"status" valid:"required,in(active|inactive|suspended)" example:"active"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` Reason string `json:"reason" valid:"required,length(10|500)" example:"Customer is active"`
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)"`
} }
// ListCustomersForm represents the form for listing customers with filters // ListCustomersForm represents the form for listing customers with filters
type ListCustomersForm struct { type SearchCustomersForm struct {
Search *string `query:"search" valid:"optional"` Search *string `query:"search" valid:"optional"`
Type *string `query:"type" valid:"optional,in(individual|company|government)"` Type *string `query:"type" valid:"optional,in(individual|company|government)"`
Status *string `query:"status" valid:"optional,in(active|inactive|suspended|pending)"` Status *string `query:"status" valid:"optional,in(active|inactive|suspended)"`
CompanyID *string `query:"company_id" valid:"optional,uuid"` CompanyID *string `query:"company_id" valid:"optional"`
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"`
} }
// CustomerListResponse represents the response for listing customers // CustomerListResponse represents the response for listing customers
type CustomerListResponse struct { type CustomerListResponse struct {
Customers []*CustomerResponse `json:"customers"` Customers []*CustomerResponse `json:"customers"`
Total int64 `json:"total"` Meta *response.Meta `json:"meta"`
Limit int `json:"limit"`
Offset int `json:"offset"`
TotalPages int `json:"total_pages"`
} }
// 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 { type LoginForm struct {
Username string `json:"username" valid:"required" example:"nakhostin"` Username string `json:"username" valid:"required" example:"app"`
Password string `json:"password" valid:"required" example:"Nima.1998"` Password string `json:"password" valid:"required" example:"App!1234"`
} }
type RefreshTokenForm struct { type RefreshTokenForm struct {
@@ -191,20 +59,11 @@ type ChangePasswordForm struct {
} }
type UpdateProfileForm struct { type UpdateProfileForm struct {
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
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)"`
} }
// Customer Authentication Response DTOs
type AuthResponse struct { type AuthResponse struct {
Customer *CustomerResponse `json:"customer"` Customer *CustomerResponse `json:"customer"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@@ -212,7 +71,6 @@ type AuthResponse struct {
ExpiresAt int64 `json:"expires_at"` ExpiresAt int64 `json:"expires_at"`
} }
// Company assignment forms
type AssignCompaniesForm struct { type AssignCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"` CompanyIDs []string `json:"company_ids" valid:"required"`
} }
+23 -445
View File
@@ -1,7 +1,6 @@
package customer package customer
import ( import (
"strconv"
"tm/internal/user" "tm/internal/user"
"tm/pkg/authorization" "tm/pkg/authorization"
"tm/pkg/logger" "tm/pkg/logger"
@@ -48,13 +47,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error()) return response.ValidationError(c, "Invalid request data", err.Error())
} }
// Get user ID from JWT token context customer, err := h.service.Register(c.Request().Context(), form)
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)
if err != nil { if err != nil {
if err.Error() == "customer with this email already exists" || if err.Error() == "customer with this email already exists" ||
err.Error() == "company with this name 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.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) // GetCustomerByID retrieves a customer by ID (Web Panel)
@@ -82,9 +75,9 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/customers/{id} [get] // @Router /admin/v1/customers/{id} [get]
func (h *Handler) GetCustomerByID(c echo.Context) error { 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 != nil {
if err.Error() == "customer not found" { if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found") return response.NotFound(c, "Customer not found")
@@ -112,19 +105,15 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/customers/{id} [put] // @Router /admin/v1/customers/{id} [put]
func (h *Handler) UpdateCustomer(c echo.Context) error { func (h *Handler) UpdateCustomer(c echo.Context) error {
idStr := c.Param("id") id := c.Param("id")
form, err := response.Parse[UpdateCustomerForm](c) form, err := response.Parse[UpdateCustomerForm](c)
if err != nil { if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error()) return response.ValidationError(c, "Invalid request data", err.Error())
} }
// Get user ID from JWT token context // 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 != nil {
if err.Error() == "customer not found" { if err.Error() == "customer not found" {
return response.NotFound(c, "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.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) // DeleteCustomer deletes a customer (Web Panel)
@@ -155,15 +144,9 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/customers/{id} [delete] // @Router /admin/v1/customers/{id} [delete]
func (h *Handler) DeleteCustomer(c echo.Context) error { func (h *Handler) DeleteCustomer(c echo.Context) error {
idStr := c.Param("id") id := c.Param("id")
// Get user ID from JWT token context err := h.service.Delete(c.Request().Context(), id)
deletedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.DeleteCustomer(c.Request().Context(), idStr, &deletedBy)
if err != nil { if err != nil {
if err.Error() == "customer not found" { if err.Error() == "customer not found" {
return response.NotFound(c, "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 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 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 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 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 offset query integer false "Number of customers to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(email, company_name, created_at, updated_at, status) default(created_at) // @Param sort_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" // @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/customers [get] // @Router /admin/v1/customers [get]
func (h *Handler) ListCustomers(c echo.Context) error { func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[ListCustomersForm](c) form, err := response.Parse[SearchCustomersForm](c)
if err != nil { if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error()) 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 { if err != nil {
return response.InternalServerError(c, "Failed to list customers") return response.InternalServerError(c, "Failed to list customers")
} }
@@ -222,7 +202,7 @@ func (h *Handler) ListCustomers(c echo.Context) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "Customer ID" // @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" // @Success 200 {object} response.APIResponse "Customer status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" // @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found" // @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" // @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth // @Security BearerAuth
// @Router /admin/v1/customers/{id}/status [patch] // @Router /admin/v1/customers/{id}/status [patch]
func (h *Handler) UpdateCustomerStatus(c echo.Context) error { func (h *Handler) UpdateStatus(c echo.Context) error {
idStr := c.Param("id") id := c.Param("id")
form, err := response.Parse[UpdateCustomerStatusForm](c)
form, err := response.Parse[UpdateStatusForm](c)
if err != nil { if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error()) return response.ValidationError(c, "Invalid request data", err.Error())
} }
// Get user ID from JWT token context err = h.service.UpdateStatus(c.Request().Context(), id, form)
updatedBy, err := user.GetUserIDFromContext(c)
if err != nil { 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") 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") }, "Customer status updated successfully")
} }
// UpdateCustomerVerification updates customer verification status (Web Panel) // **************************************************
// @Summary Update customer verification status // * PROFILE HANDLERS *
// @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")
}
// Login handles customer authentication // Login handles customer authentication
// @Summary Customer login // @Summary Customer login
+93 -285
View File
@@ -5,61 +5,46 @@ import (
"errors" "errors"
"time" "time"
"tm/pkg/logger" "tm/pkg/logger"
mongopkg "tm/pkg/mongo" orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
// Repository defines the interface for customer data operations // Repository defines the interface for customer data operations
type Repository interface { 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) GetByID(ctx context.Context, id string) (*Customer, error)
GetByEmail(ctx context.Context, email string) (*Customer, error) GetByEmail(ctx context.Context, email string) (*Customer, error)
GetByUsername(ctx context.Context, username string) (*Customer, error) GetByUsername(ctx context.Context, username string) (*Customer, error)
GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error)
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, 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
Delete(ctx context.Context, id string) 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 // customerRepository implements the Repository interface using the MongoDB ORM
type customerRepository struct { type customerRepository struct {
ormRepo mongopkg.Repository[Customer] ormRepo orm.Repository[Customer]
logger logger.Logger logger logger.Logger
} }
func collection() string {
return "customers"
}
// NewCustomerRepository creates a new customer repository // 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 // Create indexes using the ORM's index management
indexes := []mongopkg.Index{ indexes := []orm.Index{
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}), *orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}), *orm.CreateTextIndex("search_idx", "full_name", "email", "username", "phone"),
*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"),
} }
// Create indexes // Create indexes
err := mongoManager.CreateIndexes("customers", indexes) err := mongoManager.CreateIndexes(collection(), indexes)
if err != nil { if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{ logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -67,7 +52,7 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
} }
// Create ORM repository // Create ORM repository
ormRepo := mongopkg.NewRepository[Customer](mongoManager.GetCollection("customers"), logger) ormRepo := orm.NewRepository[Customer](mongoManager.GetCollection(collection()), logger)
return &customerRepository{ return &customerRepository{
ormRepo: ormRepo, ormRepo: ormRepo,
@@ -75,17 +60,16 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
} }
} }
// Create creates a new customer // Register creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error { func (r *customerRepository) Register(ctx context.Context, customer *Customer) error {
// Set created/updated timestamps using Unix timestamps // Set created/updated timestamps using Unix timestamps
now := time.Now().Unix() now := time.Now().Unix()
customer.SetCreatedAt(now) customer.SetCreatedAt(now)
customer.SetUpdatedAt(now)
// Use ORM to create customer // Use ORM to create customer
err := r.ormRepo.Create(ctx, customer) err := r.ormRepo.Create(ctx, customer)
if err != nil { 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(), "error": err.Error(),
"email": customer.Email, "email": customer.Email,
"username": customer.Username, "username": customer.Username,
@@ -93,7 +77,7 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
return 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, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
"type": customer.Type, "type": customer.Type,
@@ -102,11 +86,40 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
return nil 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 // GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) { func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) {
customer, err := r.ormRepo.FindByID(ctx, id) customer, err := r.ormRepo.FindByID(ctx, id)
if err != nil { if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) { if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found") return nil, errors.New("customer not found")
} }
r.logger.Error("Failed to get customer by ID", map[string]interface{}{ 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} filter := bson.M{"email": email}
customer, err := r.ormRepo.FindOne(ctx, filter) customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil { if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) { if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found") return nil, errors.New("customer not found")
} }
r.logger.Error("Failed to get customer by email", map[string]interface{}{ 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} filter := bson.M{"username": username}
customer, err := r.ormRepo.FindOne(ctx, filter) customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil { if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) { if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found") return nil, errors.New("customer not found")
} }
r.logger.Error("Failed to get customer by username", map[string]interface{}{ 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 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 // 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 // Build pagination
pagination := mongopkg.NewPaginationBuilder(). paginationBuilder := orm.NewPaginationBuilder().
Limit(limit). Limit(pagination.Limit).
Skip(offset). Skip(pagination.Offset).
SortDesc("created_at"). SortBy(pagination.SortBy, pagination.SortOrder).
Build() Build()
// Filter by company ID and active status // 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 // Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination) result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if err != nil { if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{ r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"company_id": companyID, "company_id": companyID,
"limit": limit, "limit": pagination.Limit,
"offset": offset, "offset": pagination.Offset,
}) })
return nil, err return nil, 0, err
} }
// Convert []Customer to []*Customer // Convert []Customer to []*Customer
@@ -242,7 +201,7 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin
customers[i] = &result.Items[i] customers[i] = &result.Items[i]
} }
return customers, nil return customers, result.TotalCount, nil
} }
// Update updates a customer // Update updates a customer
@@ -268,20 +227,24 @@ func (r *customerRepository) Update(ctx context.Context, customer *Customer) err
return nil 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 { func (r *customerRepository) Delete(ctx context.Context, id string) error {
// Get customer first // Get customer first
customer, err := r.GetByID(ctx, id) _, err := r.GetByID(ctx, id)
if err != nil { 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 return err
} }
// Update status to inactive
customer.Status = CustomerStatusInactive
customer.SetUpdatedAt(time.Now().Unix())
// Update in database // Update in database
err = r.ormRepo.Update(ctx, customer) err = r.ormRepo.Delete(ctx, id)
if err != nil { if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{ r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -297,166 +260,44 @@ func (r *customerRepository) Delete(ctx context.Context, id string) error {
return nil 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 // 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 // Build filter
filter := bson.M{} filter := bson.M{}
if search != "" { if form.Search != nil {
filter["$text"] = bson.M{"$search": search} filter["$text"] = bson.M{"$search": *form.Search}
} }
if customerType != nil { if form.Type != nil {
filter["type"] = *customerType filter["type"] = *form.Type
} }
if status != nil { if form.Status != nil {
filter["status"] = *status filter["status"] = *form.Status
} }
if companyID != nil { if form.CompanyID != nil {
filter["company_id"] = *companyID filter["company_id"] = *form.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
} }
// Build pagination // Build pagination
pagination := mongopkg.NewPaginationBuilder(). paginationBuilder := orm.NewPaginationBuilder().
Limit(limit). Limit(pagination.Limit).
Skip(offset) Skip(pagination.Offset).
SortBy(pagination.SortBy, pagination.SortOrder)
// Set sort
if sortBy != "" {
if sortOrder == "desc" {
pagination.SortDesc(sortBy)
} else {
pagination.SortAsc(sortBy)
}
} else {
pagination.SortDesc("created_at") // Default sort
}
// Use ORM to find customers // 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 { if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{ r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"search": search, "search": form.Search,
}) })
return nil, err return nil, 0, err
} }
// Convert []Customer to []*Customer return result.Items, result.TotalCount, nil
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
} }
// UpdateStatus updates customer status // UpdateStatus updates customer status
@@ -489,36 +330,3 @@ func (r *customerRepository) UpdateStatus(ctx context.Context, id string, status
return nil 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/internal/company"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/response"
"tm/pkg/authorization" "tm/pkg/authorization"
@@ -17,41 +18,18 @@ import (
// Service defines business logic for customer operations // Service defines business logic for customer operations
type Service interface { type Service interface {
// Core customer operations // Core customer operations
CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error)
GetCustomerByID(ctx context.Context, id string) (*Customer, error) GetByID(ctx context.Context, id string) (*CustomerResponse, error)
GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error)
UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error)
DeleteCustomer(ctx context.Context, id string, deletedBy *string) error Delete(ctx context.Context, id string) error
UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) 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
// Authentication operations // Authentication operations
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfile(ctx context.Context, customerID string) (*Customer, error) GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) Logout(ctx context.Context, id string, accessToken string) error
Logout(ctx context.Context, customerID string, accessToken string) error
} }
// customerService implements the Service interface // customerService implements the Service interface
@@ -72,34 +50,17 @@ func NewCustomerService(repository Repository, logger logger.Logger, authService
} }
} }
// CreateCustomer creates a new customer // Register creates a new customer
func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) { func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) {
// Check if email already exists // Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email) existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil { if existingCustomer != nil {
return nil, errors.New("customer with this email already exists") return nil, errors.New("customer with this email already exists")
} }
// Check if registration number already exists (for company customers) // Verify that all company IDs exist
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
var companyIDs []string var companyIDs []string
if len(form.CompanyIDs) > 0 { if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
for _, companyID := range form.CompanyIDs { for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID) _, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil { if err != nil {
@@ -124,36 +85,18 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
// Create customer // Create customer
customer := &Customer{ customer := &Customer{
Type: CustomerType(form.Type), Type: CustomerType(form.Type),
Status: CustomerStatusPending, Status: CustomerStatusActive,
CompanyIDs: companyIDs, CompanyIDs: companyIDs,
FirstName: form.FirstName, FullName: form.FullName,
LastName: form.LastName, Username: form.Username,
FullName: form.FullName, Email: form.Email,
Username: form.Username, Password: string(hashedPassword),
Email: form.Email, Phone: form.Phone,
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,
} }
// Save to database // Save to database
err = s.repository.Create(ctx, customer) err = s.repository.Register(ctx, customer)
if err != nil { if err != nil {
s.logger.Error("Failed to create customer", map[string]interface{}{ s.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -168,14 +111,13 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
"email": customer.Email, "email": customer.Email,
"type": customer.Type, "type": customer.Type,
"company_ids": companyIDs, "company_ids": companyIDs,
"created_by": *createdBy,
}) })
return customer, nil return customer.ToResponse(nil), nil
} }
// GetCustomerByID retrieves a customer by ID // GetByID retrieves a customer by ID
func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Customer, error) { func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResponse, error) {
customer, err := s.repository.GetByID(ctx, id) customer, err := s.repository.GetByID(ctx, id)
if err != nil { if err != nil {
s.logger.Error("Failed to get customer by ID", map[string]interface{}{ 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 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{}{ s.logger.Info("Customer retrieved successfully", map[string]interface{}{
"customer_id": customer.ID, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
}) })
return customer, nil return customer.ToResponse(companies), nil
} }
// GetCustomerByIDWithCompanies retrieves a customer by ID and loads associated companies // Search searches for customers
func (s *customerService) GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) { func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) {
customer, err := s.repository.GetByID(ctx, id) customers, total, err := s.repository.Search(ctx, form, pagination)
if err != nil { if err != nil {
s.logger.Error("Failed to get customer by ID with companies", map[string]interface{}{ s.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"customer_id": id,
}) })
return nil, err return nil, errors.New("failed to search customers")
} }
// Load companies for the customer var customerResponses []*CustomerResponse
companies, err := s.loadCompaniesForCustomer(ctx, customer) for _, customer := range customers {
if err != nil { companies, err := s.loadCompaniesForCustomer(ctx, &customer)
s.logger.Error("Failed to load companies for customer", map[string]interface{}{ if err != nil {
"error": err.Error(), s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"customer_id": customer.ID, "error": err.Error(),
}) "customer_id": customer.ID,
// Continue without companies if loading fails })
companies = []*CompanySummary{} }
customerResponses = append(customerResponses, customer.ToResponse(companies))
} }
customerResponse := customer.ToResponseWithCompanies(companies) return &CustomerListResponse{
Customers: customerResponses,
s.logger.Info("Customer retrieved with companies successfully", map[string]interface{}{ Meta: pagination.Response(total),
"customer_id": customer.ID, }, nil
"email": customer.Email,
})
return customerResponse, nil
} }
// UpdateCustomer updates a customer // Update updates a customer
func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) { func (s *customerService) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) {
// Get existing customer // Get existing customer
customer, err := s.repository.GetByID(ctx, id) customer, err := s.repository.GetByID(ctx, id)
if err != nil { 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) // Check if email already exists (if changing email)
if form.Email != nil && *form.Email != customer.Email { if form.Email != "" && form.Email != customer.Email {
existingCustomer, _ := s.repository.GetByEmail(ctx, *form.Email) existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil { if existingCustomer != nil {
return nil, errors.New("customer with this email already exists") 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) // Check if username already exists (if changing username)
if form.Username != nil && *form.Username != customer.Username { if form.Username != "" && form.Username != customer.Username {
existingCustomer, _ := s.repository.GetByUsername(ctx, *form.Username) existingCustomer, _ := s.repository.GetByUsername(ctx, form.Username)
if existingCustomer != nil { if existingCustomer != nil {
return nil, errors.New("customer with this username already exists") return nil, errors.New("customer with this username already exists")
} }
customer.Username = *form.Username 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)
} }
// Handle company assignments // Handle company assignments
@@ -296,14 +223,6 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.CompanyIDs = validCompanyIDs customer.CompanyIDs = validCompanyIDs
} }
if form.FirstName != nil {
customer.FirstName = form.FirstName
}
if form.LastName != nil {
customer.LastName = form.LastName
}
if form.FullName != nil { if form.FullName != nil {
customer.FullName = form.FullName customer.FullName = form.FullName
} }
@@ -312,54 +231,6 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.Phone = form.Phone 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 // Update in database
err = s.repository.Update(ctx, customer) err = s.repository.Update(ctx, customer)
if err != nil { 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{}{ s.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
"updated_by": *updatedBy,
}) })
return customer, nil return customer.ToResponse(nil), nil
} }
// DeleteCustomer deletes a customer (soft delete) // Delete deletes a customer (soft delete)
func (s *customerService) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error { func (s *customerService) Delete(ctx context.Context, id string) error {
// Get customer to check if exists // Get customer to check if exists
customer, err := s.repository.GetByID(ctx, id) _, err := s.repository.GetByID(ctx, id)
if err != nil { 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") return errors.New("customer not found")
} }
// Set updated by before deletion
customer.UpdatedBy = deletedBy
// Delete customer (soft delete) // Delete customer (soft delete)
err = s.repository.Delete(ctx, id) err = s.repository.Delete(ctx, id)
if err != nil { 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{}{ s.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id, "customer_id": id,
"deleted_by": deletedBy,
}) })
return nil return nil
} }
// ListCustomers lists customers with search and filters // UpdateStatus updates customer status
func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) { func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) 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 {
// Get customer to check if exists // Get customer to check if exists
_, err := s.repository.GetByID(ctx, id) _, err := s.repository.GetByID(ctx, id)
if err != nil { 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{}{ s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id, "customer_id": id,
"status": form.Status, "status": form.Status,
"updated_by": updatedBy,
}) })
return nil return nil
} }
// UpdateCustomerVerification updates customer verification status // AssignCompanies assigns companies to a customer
func (s *customerService) UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error { func (s *customerService) AssignCompanies(ctx context.Context, customerID string, companyIDs []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 {
// Get customer to check if exists // Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID) customer, err := s.repository.GetByID(ctx, customerID)
if err != nil { if err != nil {
@@ -630,7 +331,6 @@ func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, custome
// Update customer's company IDs // Update customer's company IDs
customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...) customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...)
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix() customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer) 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{}{ s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{
"customer_id": customerID, "customer_id": customerID,
"company_ids": validCompanyIDs, "company_ids": validCompanyIDs,
"updated_by": updatedBy,
}) })
return nil return nil
} }
// RemoveCompaniesFromCustomer removes companies from a customer // 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 // Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID) customer, err := s.repository.GetByID(ctx, customerID)
if err != nil { if err != nil {
@@ -676,7 +375,6 @@ func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, custo
// Update customer's company IDs // Update customer's company IDs
customer.CompanyIDs = updatedCompanyIDs customer.CompanyIDs = updatedCompanyIDs
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix() customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer) 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{}{ s.logger.Info("Companies removed from customer successfully", map[string]interface{}{
"customer_id": customerID, "customer_id": customerID,
"company_ids": companyIDs, "company_ids": companyIDs,
"updated_by": updatedBy,
}) })
return nil return nil
} }
// CreateCustomerMobile creates a new customer via mobile app (simplified) // Suspend suspends a customer
func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) { func (s *customerService) Suspend(ctx context.Context, id string, reason string) 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 {
// Get customer to check if exists // Get customer to check if exists
_, err := s.repository.GetByID(ctx, id) _, err := s.repository.GetByID(ctx, id)
if err != nil { 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{}{ s.logger.Info("Customer suspended successfully", map[string]interface{}{
"customer_id": id, "customer_id": id,
"reason": reason, "reason": reason,
"suspended_by": suspendedBy,
}) })
return nil return nil
} }
// ActivateCustomer activates a customer // Activate activates a customer
func (s *customerService) ActivateCustomer(ctx context.Context, id string, activatedBy *string) error { func (s *customerService) Activate(ctx context.Context, id string) error {
// Get customer to check if exists // Get customer to check if exists
_, err := s.repository.GetByID(ctx, id) _, err := s.repository.GetByID(ctx, id)
if err != nil { 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{}{ s.logger.Info("Customer activated successfully", map[string]interface{}{
"customer_id": id, "customer_id": id,
"activated_by": activatedBy,
}) })
return nil 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") 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{}{ s.logger.Info("Customer login successful", map[string]interface{}{
"username": form.Username, "username": form.Username,
"customer_id": customer.ID, "customer_id": customer.ID,
@@ -992,7 +523,7 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
}) })
return &AuthResponse{ return &AuthResponse{
Customer: customer.ToResponse(), Customer: customer.ToResponse(companies),
AccessToken: tokenPair.AccessToken, AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken, RefreshToken: tokenPair.RefreshToken,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now 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") 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{}{ s.logger.Info("Access token refreshed successfully", map[string]interface{}{
"customer_id": customer.ID, "customer_id": customer.ID,
"customer_type": string(customer.Type), "customer_type": string(customer.Type),
}) })
return &AuthResponse{ return &AuthResponse{
Customer: customer.ToResponse(), Customer: customer.ToResponse(companies),
AccessToken: newAccessToken, AccessToken: newAccessToken,
RefreshToken: refreshToken, // Return the same refresh token RefreshToken: refreshToken, // Return the same refresh token
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now 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{} companies = []*CompanySummary{}
} }
customerResponse := customer.ToResponseWithCompanies(companies) customerResponse := customer.ToResponse(companies)
s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{ s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{
"customer_id": customerID, "customer_id": customerID,
@@ -1149,61 +688,6 @@ func (s *customerService) Logout(ctx context.Context, customerID string, accessT
return nil 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 // loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) { func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
if len(customer.CompanyIDs) == 0 { if len(customer.CompanyIDs) == 0 {