Refactor customer domain to use string IDs and integrate MongoDB ORM

- Updated Customer entity to embed MongoDB model and replace uuid.UUID with string for ID fields.
- Modified repository methods to accept string IDs instead of uuid.UUID, enhancing compatibility with MongoDB.
- Refactored service and handler methods to align with the new ID type, ensuring consistent usage across customer management functionality.
- Improved response handling in CustomerResponse to directly use string IDs.
- Streamlined index creation in the customer repository using the MongoDB ORM for better maintainability.
- Added new forms for customer suspension and updated validation rules.
- Enhanced Swagger documentation for customer-related endpoints to reflect changes in ID handling and request structures.
This commit is contained in:
n.nakhostin
2025-08-11 14:01:36 +03:30
parent 08cf927294
commit ce4f7e83d3
6 changed files with 604 additions and 618 deletions
+80 -67
View File
@@ -1,7 +1,7 @@
package customer
import (
"github.com/google/uuid"
"tm/pkg/mongo"
)
// CustomerStatus represents customer account status
@@ -25,77 +25,105 @@ const (
// Customer represents a customer in the tender management system
type Customer struct {
ID uuid.UUID `bson:"_id"`
Type CustomerType `bson:"type"`
Status CustomerStatus `bson:"status"`
CompanyID *uuid.UUID `bson:"company_id,omitempty"`
mongo.Model
Type CustomerType `bson:"type" json:"type"`
Status CustomerStatus `bson:"status" json:"status"`
CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"`
// Individual customer fields
FirstName *string `bson:"first_name,omitempty"`
LastName *string `bson:"last_name,omitempty"`
FullName *string `bson:"full_name,omitempty"`
Username string `bson:"username"` // Username for authentication
Email string `bson:"email"`
Password string `bson:"password"` // Hashed password for authentication
Phone *string `bson:"phone,omitempty"`
Mobile *string `bson:"mobile,omitempty"`
FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"`
LastName *string `bson:"last_name,omitempty" json:"last_name,omitempty"`
FullName *string `bson:"full_name,omitempty" json:"full_name,omitempty"`
Username string `bson:"username" json:"username"` // Username for authentication
Email string `bson:"email" json:"email"`
Password string `bson:"password" json:"-"` // Hashed password for authentication
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
// Company customer fields
CompanyName *string `bson:"company_name,omitempty"`
RegistrationNumber *string `bson:"registration_number,omitempty"`
TaxID *string `bson:"tax_id,omitempty"`
Industry *string `bson:"industry,omitempty"`
CompanyName *string `bson:"company_name,omitempty" json:"company_name,omitempty"`
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"`
Address *Address `bson:"address,omitempty" json:"address,omitempty"`
// Business information
BusinessType *string `bson:"business_type,omitempty"`
EmployeeCount *int `bson:"employee_count,omitempty"`
AnnualRevenue *float64 `bson:"annual_revenue,omitempty"`
FoundedYear *int `bson:"founded_year,omitempty"`
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"`
ContactPerson *ContactPerson `bson:"contact_person,omitempty" json:"contact_person,omitempty"`
// Verification and compliance
IsVerified bool `bson:"is_verified"`
IsCompliant bool `bson:"is_compliant"`
ComplianceNotes *string `bson:"compliance_notes,omitempty"`
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"`
// Preferences and settings
Language string `bson:"language"` // Default: "en"
Currency string `bson:"currency"` // Default: "USD"
Timezone string `bson:"timezone"` // Default: "UTC"
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
CreatedAt int64 `bson:"created_at"` // Unix timestamp
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
CreatedBy *uuid.UUID `bson:"created_by,omitempty"`
UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"`
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)
func (c *Customer) SetID(id string) {
c.ID = id
}
// GetID returns the customer ID (implements IDGetter interface)
func (c *Customer) GetID() string {
return c.ID
}
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
func (c *Customer) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
func (c *Customer) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestampable interface)
func (c *Customer) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface)
func (c *Customer) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// Address represents a customer's address
type Address struct {
Street string `bson:"street"`
City string `bson:"city"`
State string `bson:"state"`
PostalCode string `bson:"postal_code"`
Country string `bson:"country"`
AddressType string `bson:"address_type"` // billing, shipping, etc.
IsDefault bool `bson:"is_default"`
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"`
LastName string `bson:"last_name"`
FullName string `bson:"full_name"`
Position string `bson:"position"`
Email string `bson:"email"`
Phone string `bson:"phone"`
Mobile *string `bson:"mobile,omitempty"`
IsPrimary bool `bson:"is_primary"`
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"`
}
// CustomerResponse represents the customer data sent in API responses
@@ -135,26 +163,11 @@ type CustomerResponse struct {
// ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse {
companyID := ""
if c.CompanyID != nil {
companyID = c.CompanyID.String()
}
createdBy := ""
if c.CreatedBy != nil {
createdBy = c.CreatedBy.String()
}
updatedBy := ""
if c.UpdatedBy != nil {
updatedBy = c.UpdatedBy.String()
}
return &CustomerResponse{
ID: c.ID.String(),
ID: c.ID,
Type: string(c.Type),
Status: string(c.Status),
CompanyID: &companyID,
CompanyID: c.CompanyID,
FirstName: c.FirstName,
LastName: c.LastName,
FullName: c.FullName,
@@ -180,7 +193,7 @@ func (c *Customer) ToResponse() *CustomerResponse {
Timezone: c.Timezone,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
CreatedBy: &createdBy,
UpdatedBy: &updatedBy,
CreatedBy: c.CreatedBy,
UpdatedBy: c.UpdatedBy,
}
}
+6 -1
View File
@@ -99,13 +99,18 @@ type UpdateCustomerStatusForm struct {
Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"`
}
// UpdateCustomerVerificationForm represents the form for updating customer verification
// 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)"`
+155 -158
View File
@@ -7,7 +7,6 @@ import (
"tm/pkg/logger"
"tm/pkg/response"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
@@ -81,8 +80,11 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// TODO: Get user ID from JWT token
createdBy := uuid.New() // This should come from JWT token
// Get user ID from JWT token context
createdBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
customer, err := h.service.CreateCustomer(c.Request().Context(), form, &createdBy)
if err != nil {
@@ -98,32 +100,28 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
return response.Created(c, customer.ToResponse(), "Customer created successfully")
}
// GetCustomerByID retrieves a customer by ID
// GetCustomerByID retrieves a customer by ID (Web Panel)
// @Summary Get customer by ID
// @Description Retrieve detailed customer information by unique customer ID. Returns comprehensive customer data including personal details, company information, address, verification status, and audit fields.
// @Description Retrieve detailed customer information by customer ID
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID format"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [get]
func (h *Handler) GetCustomerByID(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
customer, err := h.service.GetCustomerByID(c.Request().Context(), id)
customer, err := h.service.GetCustomerByID(c.Request().Context(), idStr)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to retrieve customer")
return response.InternalServerError(c, "Failed to get customer")
}
return response.Success(c, customer.ToResponse(), "Customer retrieved successfully")
@@ -131,15 +129,15 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
// UpdateCustomer updates a customer (Web Panel)
// @Summary Update customer information
// @Description Update existing customer information with comprehensive details. All fields are optional and only provided fields will be updated. This endpoint is used by the web panel for full customer management.
// @Description Update customer information including personal details, company information, address, and business details
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param customer body UpdateCustomerForm true "Customer update information - all fields are optional"
// @Param id path string true "Customer ID"
// @Param customer body UpdateCustomerForm true "Updated customer information"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
@@ -147,20 +145,18 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
// @Router /admin/v1/customers/{id} [put]
func (h *Handler) UpdateCustomer(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
form, err := response.Parse[UpdateCustomerForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// TODO: Get user ID from JWT token
updatedBy := uuid.New() // This should come from JWT token
// 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(), id, form, &updatedBy)
customer, err := h.service.UpdateCustomer(c.Request().Context(), idStr, form, &updatedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
@@ -177,30 +173,29 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
return response.Success(c, customer.ToResponse(), "Customer updated successfully")
}
// DeleteCustomer deletes a customer (soft delete)
// DeleteCustomer deletes a customer (Web Panel)
// @Summary Delete customer
// @Description Soft delete a customer by setting status to inactive. The customer record is preserved but marked as deleted. This is a reversible operation.
// @Description Soft delete a customer by setting status to inactive
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse "Customer deleted successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID format"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id} [delete]
func (h *Handler) DeleteCustomer(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
// Get user ID from JWT token context
deletedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
return response.Unauthorized(c, "User not authenticated")
}
// TODO: Get user ID from JWT token
deletedBy := uuid.New() // This should come from JWT token
err = h.service.DeleteCustomer(c.Request().Context(), id, &deletedBy)
err = h.service.DeleteCustomer(c.Request().Context(), idStr, &deletedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
@@ -208,7 +203,9 @@ func (h *Handler) DeleteCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to delete customer")
}
return response.Success(c, nil, "Customer deleted successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer deleted successfully",
}, "Customer deleted successfully")
}
// ListCustomers lists customers with filters and pagination
@@ -250,37 +247,35 @@ func (h *Handler) ListCustomers(c echo.Context) error {
return response.Success(c, result, "Customers retrieved successfully")
}
// UpdateCustomerStatus updates customer status
// UpdateCustomerStatus updates customer status (Web Panel)
// @Summary Update customer status
// @Description Update the status of a customer account. Valid statuses include active, inactive, suspended, and pending. This affects the customer's ability to access the system.
// @Description Update customer account status (active, inactive, suspended, pending)
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param status body UpdateCustomerStatusForm true "Status update information"
// @Param id path string true "Customer ID"
// @Param status body UpdateCustomerStatusForm true "New customer status"
// @Success 200 {object} response.APIResponse "Customer status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or status"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid status value"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/status [patch]
func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
form, err := response.Parse[UpdateCustomerStatusForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// TODO: Get user ID from JWT token
updatedBy := uuid.New() // This should come from JWT token
// 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.UpdateCustomerStatus(c.Request().Context(), id, form, &updatedBy)
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")
@@ -288,40 +283,40 @@ func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
return response.InternalServerError(c, "Failed to update customer status")
}
return response.Success(c, nil, "Customer status updated successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer status updated successfully",
}, "Customer status updated successfully")
}
// UpdateCustomerVerification updates customer verification status
// @Summary Update customer verification and compliance status
// @Description Update the verification and compliance status of a customer. This includes marking customers as verified and compliant, with optional compliance notes for audit purposes.
// UpdateCustomerVerification updates customer verification status (Web Panel)
// @Summary Update customer verification status
// @Description Update customer verification and compliance status
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param verification body UpdateCustomerVerificationForm true "Verification and compliance update information"
// @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 customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid verification data"
// @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")
id, err := uuid.Parse(idStr)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
}
form, err := response.Parse[UpdateCustomerVerificationForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// TODO: Get user ID from JWT token
updatedBy := uuid.New() // This should come from JWT token
// 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(), id, form, &updatedBy)
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")
@@ -329,33 +324,34 @@ func (h *Handler) UpdateCustomerVerification(c echo.Context) error {
return response.InternalServerError(c, "Failed to update customer verification")
}
return response.Success(c, nil, "Customer verification updated successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer verification updated successfully",
}, "Customer verification updated successfully")
}
// VerifyCustomer verifies a customer
// VerifyCustomer verifies a customer (Web Panel)
// @Summary Verify customer
// @Description Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.
// @Description Mark a customer as verified
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @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 does not exist"
// @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")
id, err := uuid.Parse(idStr)
// Get user ID from JWT token context
verifiedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
return response.Unauthorized(c, "User not authenticated")
}
// TODO: Get user ID from JWT token
verifiedBy := uuid.New() // This should come from JWT token
err = h.service.VerifyCustomer(c.Request().Context(), id, &verifiedBy)
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")
@@ -363,44 +359,40 @@ func (h *Handler) VerifyCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to verify customer")
}
return response.Success(c, nil, "Customer verified successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer verified successfully",
}, "Customer verified successfully")
}
// SuspendCustomer suspends a customer
// @Summary Suspend customer account
// @Description Suspend a customer account by setting their status to suspended. Suspended customers cannot access the system. A reason for suspension must be provided.
// SuspendCustomer suspends a customer (Web Panel)
// @Summary Suspend customer
// @Description Suspend a customer account with optional reason
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @Param reason body object true "Suspension reason" SchemaExample({"reason": "Violation of terms of service"})
// @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 customer ID or missing reason"
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
// @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")
id, err := uuid.Parse(idStr)
form, err := response.Parse[SuspendCustomerForm](c)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
return response.ValidationError(c, "Invalid request data", err.Error())
}
var requestBody map[string]string
if err := c.Bind(&requestBody); err != nil {
return response.BadRequest(c, "Invalid request body", err.Error())
// Get user ID from JWT token context
suspendedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
reason := requestBody["reason"]
if reason == "" {
return response.BadRequest(c, "Suspension reason is required", "")
}
// TODO: Get user ID from JWT token
suspendedBy := uuid.New() // This should come from JWT token
err = h.service.SuspendCustomer(c.Request().Context(), id, reason, &suspendedBy)
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")
@@ -408,33 +400,34 @@ func (h *Handler) SuspendCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to suspend customer")
}
return response.Success(c, nil, "Customer suspended successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer suspended successfully",
}, "Customer suspended successfully")
}
// ActivateCustomer activates a customer
// @Summary Activate suspended customer account
// @Description Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.
// ActivateCustomer activates a customer (Web Panel)
// @Summary Activate customer
// @Description Activate a suspended customer account
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer UUID" format(uuid)
// @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 does not exist"
// @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")
id, err := uuid.Parse(idStr)
// Get user ID from JWT token context
activatedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Invalid customer ID", err.Error())
return response.Unauthorized(c, "User not authenticated")
}
// TODO: Get user ID from JWT token
activatedBy := uuid.New() // This should come from JWT token
err = h.service.ActivateCustomer(c.Request().Context(), id, &activatedBy)
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")
@@ -442,61 +435,61 @@ func (h *Handler) ActivateCustomer(c echo.Context) error {
return response.InternalServerError(c, "Failed to activate customer")
}
return response.Success(c, nil, "Customer activated successfully")
return response.Success(c, map[string]interface{}{
"message": "Customer activated successfully",
}, "Customer activated successfully")
}
// GetCustomersByCompanyID retrieves customers by company ID
// GetCustomersByCompanyID retrieves customers by company ID (Web Panel)
// @Summary Get customers by company ID
// @Description Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.
// @Description Retrieve all customers associated with a specific company
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param companyId path string true "Company UUID" format(uuid)
// @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 company ID format"
// @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")
companyID, err := uuid.Parse(companyIDStr)
if err != nil {
return response.BadRequest(c, "Invalid company ID", err.Error())
}
limit := 20
// 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
}
}
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.GetCustomersByCompanyID(c.Request().Context(), companyID, limit, offset)
customers, total, err := h.service.GetCustomersByCompanyID(c.Request().Context(), companyIDStr, limit, offset)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve customers by company")
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())
}
meta := &response.Meta{
Total: int(total),
Limit: limit,
Offset: offset,
}
return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully")
return response.Success(c, map[string]interface{}{
"customers": customerResponses,
"total": total,
"limit": limit,
"offset": offset,
}, "Customers retrieved successfully")
}
// GetCustomersByType retrieves customers by type
@@ -676,66 +669,70 @@ func (h *Handler) RefreshToken(c echo.Context) error {
return response.Success(c, authResponse, "Token refreshed successfully")
}
// GetProfile retrieves customer profile information
// GetProfile retrieves customer profile information (Mobile)
// @Summary Get customer profile
// @Description Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.
// @Tags Customers-Authorization
// @Description Retrieve current customer profile information
// @Tags Customers-Mobile
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing access token"
// @Failure 404 {object} response.APIResponse "Not found - Customer profile not found"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile [get]
func (h *Handler) GetProfile(c echo.Context) error {
// Get customer ID from context (set by AuthMiddleware)
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer ID not found in context")
return response.Unauthorized(c, "Customer not authenticated")
}
// Call service
customer, err := h.service.GetProfile(c.Request().Context(), customerID)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to retrieve customer profile")
return response.InternalServerError(c, "Failed to get customer profile")
}
return response.Success(c, customer.ToResponse(), "Profile retrieved successfully")
}
// Logout handles customer logout
// Logout handles customer logout (Mobile)
// @Summary Customer logout
// @Description Logout customer and invalidate access token. This ensures the token cannot be used for future requests.
// @Tags Customers-Authorization
// @Description Logout customer and invalidate access token
// @Tags Customers-Mobile
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse "Logout successful"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing access token"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/logout [delete]
func (h *Handler) Logout(c echo.Context) error {
// Get customer ID from context (set by AuthMiddleware)
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer ID not found in context")
return response.Unauthorized(c, "Customer not authenticated")
}
// Get access token from context
accessToken, err := GetAccessTokenFromContext(c)
if err != nil {
return response.Unauthorized(c, "Access token not found in context")
return response.Unauthorized(c, "Access token not found")
}
// Call service
err = h.service.Logout(c.Request().Context(), customerID, accessToken)
if err != nil {
return response.InternalServerError(c, "Failed to logout customer")
// Don't return error here as the customer should still be logged out
h.logger.Warn("Failed to invalidate access token during logout", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
}
return response.Success(c, nil, "Logout successful")
return response.Success(c, map[string]interface{}{
"message": "Logout successful",
}, "Logout successful")
}
+4 -11
View File
@@ -5,7 +5,6 @@ import (
"strings"
"tm/pkg/response"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
@@ -44,13 +43,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
}
// Extract customer information from token
customerID, err := uuid.Parse(validationResult.Claims.UserID)
if err != nil {
h.logger.Error("Failed to parse customer ID from token", map[string]interface{}{
"error": err.Error(),
})
return response.Unauthorized(c, "Invalid token format")
}
customerID := validationResult.Claims.UserID
// Store customer information in context for handlers to use
c.Set("customer_id", customerID)
@@ -66,10 +59,10 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
}
// GetCustomerIDFromContext extracts customer ID from Echo context
func GetCustomerIDFromContext(c echo.Context) (uuid.UUID, error) {
customerID, ok := c.Get("customer_id").(uuid.UUID)
func GetCustomerIDFromContext(c echo.Context) (string, error) {
customerID, ok := c.Get("customer_id").(string)
if !ok {
return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "Customer ID not found in context")
return "", echo.NewHTTPError(http.StatusUnauthorized, "Customer ID not found in context")
}
return customerID, nil
}
+264 -265
View File
@@ -7,162 +7,94 @@ import (
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Repository defines the interface for customer data operations
type Repository interface {
Create(ctx context.Context, customer *Customer) error
GetByID(ctx context.Context, id uuid.UUID) (*Customer, error)
GetByID(ctx context.Context, id string) (*Customer, error)
GetByEmail(ctx context.Context, email string) (*Customer, error)
GetByUsername(ctx context.Context, username string) (*Customer, error)
GetByCompanyName(ctx context.Context, companyName string) (*Customer, error)
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error)
GetByTaxID(ctx context.Context, taxID string) (*Customer, error)
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*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 uuid.UUID) 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 *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, 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 uuid.UUID, status CustomerStatus) error
UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) 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
// customerRepository implements the Repository interface using the MongoDB ORM
type customerRepository struct {
collection *mongo.Collection
logger logger.Logger
ormRepo mongopkg.Repository[Customer]
logger logger.Logger
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
collection := mongoManager.GetCollection("customers")
// Create indexes
repo := &customerRepository{
collection: collection,
logger: logger,
// Create indexes using the ORM's index management
indexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
*mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", Value: 1}}),
*mongopkg.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
*mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
*mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
*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"),
}
repo.createIndexes()
return repo
}
// createIndexes creates necessary database indexes
func (r *customerRepository) createIndexes() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Create indexes
indexes := []mongo.IndexModel{
{
Keys: bson.D{
{Key: "email", Value: 1},
},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{
{Key: "username", Value: 1},
},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{
{Key: "company_name", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "registration_number", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "tax_id", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "company_id", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "type", Value: 1},
},
},
{
Keys: bson.D{
{Key: "status", Value: 1},
},
},
{
Keys: bson.D{
{Key: "industry", Value: 1},
},
},
{
Keys: bson.D{
{Key: "created_at", Value: -1},
},
},
{
Keys: bson.D{
{Key: "updated_at", Value: -1},
},
},
}
_, err := r.collection.Indexes().CreateMany(ctx, indexes)
err := mongoManager.CreateIndexes("customers", indexes)
if err != nil {
r.logger.Error("Failed to create customer indexes", map[string]interface{}{
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
} else {
r.logger.Info("Customer indexes created successfully", map[string]interface{}{})
}
// Create ORM repository
ormRepo := mongopkg.NewRepository[Customer](mongoManager.GetCollection("customers"), logger)
return &customerRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
// Set timestamps
// Set created/updated timestamps using Unix timestamps
now := time.Now().Unix()
customer.CreatedAt = now
customer.UpdatedAt = now
customer.SetCreatedAt(now)
customer.SetUpdatedAt(now)
// Set defaults if not provided
if customer.Language == "" {
customer.Language = "en"
}
if customer.Currency == "" {
customer.Currency = "USD"
}
if customer.Timezone == "" {
customer.Timezone = "UTC"
}
_, err := r.collection.InsertOne(ctx, customer)
// Use ORM to create customer
err := r.ormRepo.Create(ctx, customer)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return errors.New("customer with this email already exists")
}
r.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": customer.Email,
"username": customer.Username,
})
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
})
@@ -171,107 +103,143 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&customer)
func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) {
customer, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if err == mongo.ErrNoDocuments {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, err
}
return &customer, nil
return customer, nil
}
// GetByEmail retrieves a customer by email
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&customer)
filter := bson.M{"email": email}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by email", map[string]interface{}{
"error": err.Error(),
"email": email,
})
return nil, err
}
return &customer, nil
return customer, nil
}
// GetByUsername retrieves a customer by username
func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"username": username}).Decode(&customer)
filter := bson.M{"username": username}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by username", map[string]interface{}{
"error": err.Error(),
"username": username,
})
return nil, err
}
return &customer, nil
return customer, nil
}
// GetByCompanyName retrieves a customer by company name
func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"company_name": companyName}).Decode(&customer)
filter := bson.M{"company_name": companyName}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
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
return customer, nil
}
// GetByRegistrationNumber retrieves a customer by registration number
func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"registration_number": registrationNumber}).Decode(&customer)
filter := bson.M{"registration_number": registrationNumber}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
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
return customer, nil
}
// GetByTaxID retrieves a customer by tax ID
func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"tax_id": taxID}).Decode(&customer)
filter := bson.M{"tax_id": taxID}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
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
return customer, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
filter := bson.M{"company_id": companyID}
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: "created_at", Value: -1}})
// Filter by company ID and active status
filter := bson.M{
"company_id": companyID,
"status": bson.M{"$ne": CustomerStatusInactive},
}
cursor, err := r.collection.Find(ctx, filter, opts)
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
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
@@ -279,22 +247,21 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
customer.UpdatedAt = time.Now().Unix()
// Set updated timestamp using Unix timestamp
customer.SetUpdatedAt(time.Now().Unix())
filter := bson.M{"_id": customer.ID}
update := bson.M{"$set": customer}
result, err := r.collection.UpdateOne(ctx, filter, update)
// Use ORM to update customer
err := r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
})
@@ -302,26 +269,29 @@ func (r *customerRepository) Update(ctx context.Context, customer *Customer) err
}
// Delete deletes a customer (soft delete by setting status to inactive)
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"status": CustomerStatusInactive,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
func (r *customerRepository) Delete(ctx context.Context, id string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
// Update status to inactive
customer.Status = CustomerStatusInactive
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
r.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id.String(),
"customer_id": id,
})
return nil
@@ -329,124 +299,125 @@ func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: "created_at", Value: -1}})
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
// 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
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
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 searches customers with filters
func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
// 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) {
// Build filter
filter := bson.M{}
// Add search filter
if search != "" {
filter["$or"] = []bson.M{
{"email": primitive.Regex{Pattern: search, Options: "i"}},
{"company_name": primitive.Regex{Pattern: search, Options: "i"}},
{"first_name": primitive.Regex{Pattern: search, Options: "i"}},
{"last_name": primitive.Regex{Pattern: search, Options: "i"}},
{"full_name": primitive.Regex{Pattern: search, Options: "i"}},
}
filter["$text"] = bson.M{"$search": search}
}
// Add type filter
if customerType != nil {
filter["type"] = *customerType
}
// Add status filter
if status != nil {
filter["status"] = *status
}
// Add company ID filter
if companyID != nil {
filter["company_id"] = *companyID
}
// Add industry filter
if industry != nil {
filter["industry"] = *industry
}
// Add verification filter
if isVerified != nil {
filter["is_verified"] = *isVerified
}
// Add compliance filter
if isCompliant != nil {
filter["is_compliant"] = *isCompliant
}
// Add language filter
if language != nil {
filter["language"] = *language
}
// Add currency filter
if currency != nil {
filter["currency"] = *currency
}
// Set sort options
sortDirection := 1
if sortOrder == "desc" {
sortDirection = -1
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset)
// Set sort
if sortBy != "" {
if sortOrder == "desc" {
pagination.SortDesc(sortBy)
} else {
pagination.SortAsc(sortBy)
}
} else {
pagination.SortDesc("created_at") // Default sort
}
var sortField string
switch sortBy {
case "email":
sortField = "email"
case "company_name":
sortField = "company_name"
case "updated_at":
sortField = "updated_at"
case "status":
sortField = "status"
default:
sortField = "created_at"
}
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: sortField, Value: sortDirection}})
cursor, err := r.collection.Find(ctx, filter, opts)
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": search,
})
return nil, err
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
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
}
// CountByCompanyID counts customers by company ID
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) {
filter := bson.M{"company_id": companyID}
count, err := r.collection.CountDocuments(ctx, filter)
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
}
@@ -455,9 +426,17 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uui
// CountByType counts customers by type
func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) {
filter := bson.M{"type": customerType}
count, err := r.collection.CountDocuments(ctx, filter)
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
}
@@ -467,8 +446,13 @@ func (r *customerRepository) CountByType(ctx context.Context, customerType Custo
// CountByStatus counts customers by status
func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) {
filter := bson.M{"status": status}
count, err := r.collection.CountDocuments(ctx, filter)
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
}
@@ -476,50 +460,65 @@ func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerS
}
// UpdateStatus updates customer status
func (r *customerRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"status": status,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
func (r *customerRepository) UpdateStatus(ctx context.Context, id string, status CustomerStatus) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
// Update status
customer.Status = status
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to update customer status", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
"status": status,
})
return err
}
r.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id,
"status": status,
})
return nil
}
// UpdateVerification updates customer verification status
func (r *customerRepository) UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"is_verified": isVerified,
"is_compliant": isCompliant,
"updated_at": time.Now().Unix(),
},
}
if complianceNotes != nil {
update["$set"].(bson.M)["compliance_notes"] = *complianceNotes
}
result, err := r.collection.UpdateOne(ctx, filter, update)
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
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
// 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
}
+95 -116
View File
@@ -16,35 +16,35 @@ import (
// Service defines business logic for customer operations
type Service interface {
// Core customer operations
CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *uuid.UUID) (*Customer, error)
GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error)
UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm, updatedBy *uuid.UUID) (*Customer, error)
DeleteCustomer(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error
CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error)
GetCustomerByID(ctx context.Context, id string) (*Customer, error)
UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error)
DeleteCustomer(ctx context.Context, id string, deletedBy *string) error
// Customer listing and search
ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error)
GetCustomersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, int64, 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 uuid.UUID, form *UpdateCustomerStatusForm, updatedBy *uuid.UUID) error
UpdateCustomerVerification(ctx context.Context, id uuid.UUID, form *UpdateCustomerVerificationForm, updatedBy *uuid.UUID) error
UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error
UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error
// Mobile-specific operations (simplified)
CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *uuid.UUID) (*Customer, error)
UpdateCustomerMobile(ctx context.Context, id uuid.UUID, form *UpdateCustomerMobileForm, updatedBy *uuid.UUID) (*Customer, error)
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 uuid.UUID, verifiedBy *uuid.UUID) error
SuspendCustomer(ctx context.Context, id uuid.UUID, reason string, suspendedBy *uuid.UUID) error
ActivateCustomer(ctx context.Context, id uuid.UUID, activatedBy *uuid.UUID) error
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
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfile(ctx context.Context, customerID uuid.UUID) (*Customer, error)
Logout(ctx context.Context, customerID uuid.UUID, accessToken string) error
GetProfile(ctx context.Context, customerID string) (*Customer, error)
Logout(ctx context.Context, customerID string, accessToken string) error
}
// customerService implements the Service interface
@@ -64,7 +64,7 @@ func NewCustomerService(repository Repository, logger logger.Logger, authService
}
// CreateCustomer creates a new customer
func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *uuid.UUID) (*Customer, error) {
func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
@@ -96,27 +96,31 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
}
// Parse company ID if provided
var companyID *uuid.UUID
var companyID *string
if form.CompanyID != nil {
parsedID, err := uuid.Parse(*form.CompanyID)
if err != nil {
return nil, errors.New("invalid company ID")
}
companyID = &parsedID
companyID = form.CompanyID
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash password", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to process password")
}
// Create customer
customer := &Customer{
ID: uuid.New(),
Type: CustomerType(form.Type),
Status: CustomerStatusActive,
Status: CustomerStatusPending,
CompanyID: companyID,
FirstName: form.FirstName,
LastName: form.LastName,
FullName: form.FullName,
Username: form.Username,
Email: form.Email,
Password: "", // Will be set after hashing
Password: string(hashedPassword),
Phone: form.Phone,
Mobile: form.Mobile,
CompanyName: form.CompanyName,
@@ -134,56 +138,43 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
Language: s.getDefaultLanguage(form.Language),
Currency: s.getDefaultCurrency(form.Currency),
Timezone: s.getDefaultTimezone(form.Timezone),
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
CreatedBy: createdBy,
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash password", 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", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
"type": form.Type,
"created_by": createdBy.String(),
"error": err.Error(),
"email": form.Email,
"username": form.Username,
})
return nil, err
}
s.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"created_by": createdBy.String(),
"created_by": *createdBy,
})
return customer, nil
}
// GetCustomerByID retrieves a customer by ID
func (s *customerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Customer, error) {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return nil, err
}
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
})
@@ -191,13 +182,13 @@ func (s *customerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*C
}
// UpdateCustomer updates a customer
func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm, updatedBy *uuid.UUID) (*Customer, error) {
func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) {
// Get existing customer
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer for update", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return nil, errors.New("customer not found")
}
@@ -253,11 +244,7 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form
}
if form.CompanyID != nil {
parsedID, err := uuid.Parse(*form.CompanyID)
if err != nil {
return nil, errors.New("invalid company ID")
}
customer.CompanyID = &parsedID
customer.CompanyID = form.CompanyID
}
if form.FirstName != nil {
@@ -329,22 +316,22 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form
if err != nil {
s.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return nil, errors.New("failed to update customer")
}
s.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
"updated_by": updatedBy.String(),
"updated_by": *updatedBy,
})
return customer, nil
}
// DeleteCustomer deletes a customer (soft delete)
func (s *customerService) DeleteCustomer(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error {
func (s *customerService) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -359,14 +346,14 @@ func (s *customerService) DeleteCustomer(ctx context.Context, id uuid.UUID, dele
if err != nil {
s.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return errors.New("failed to delete customer")
}
s.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id.String(),
"deleted_by": deletedBy.String(),
"customer_id": id,
"deleted_by": deletedBy,
})
return nil
@@ -401,13 +388,9 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers
}
// Parse company ID if provided
var companyID *uuid.UUID
var companyID *string
if form.CompanyID != nil {
parsedID, err := uuid.Parse(*form.CompanyID)
if err != nil {
return nil, errors.New("invalid company ID")
}
companyID = &parsedID
companyID = form.CompanyID
}
// Get customers
@@ -439,12 +422,12 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers
}
// GetCustomersByCompanyID retrieves customers by company ID with pagination
func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, int64, error) {
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.String(),
"company_id": companyID,
})
return nil, 0, errors.New("failed to get customers by company")
}
@@ -454,7 +437,7 @@ func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID
if err != nil {
s.logger.Error("Failed to count customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
"company_id": companyID,
})
return customers, 0, nil // Return customers even if count fails
}
@@ -517,7 +500,7 @@ func (s *customerService) GetCustomersByStatus(ctx context.Context, status Custo
}
// UpdateCustomerStatus updates customer status
func (s *customerService) UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm, updatedBy *uuid.UUID) error {
func (s *customerService) UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -530,23 +513,23 @@ func (s *customerService) UpdateCustomerStatus(ctx context.Context, id uuid.UUID
if err != nil {
s.logger.Error("Failed to update customer status", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
"status": form.Status,
})
return errors.New("failed to update customer status")
}
s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": id.String(),
"customer_id": id,
"status": form.Status,
"updated_by": updatedBy.String(),
"updated_by": updatedBy,
})
return nil
}
// UpdateCustomerVerification updates customer verification status
func (s *customerService) UpdateCustomerVerification(ctx context.Context, id uuid.UUID, form *UpdateCustomerVerificationForm, updatedBy *uuid.UUID) error {
func (s *customerService) UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -558,23 +541,23 @@ func (s *customerService) UpdateCustomerVerification(ctx context.Context, id uui
if err != nil {
s.logger.Error("Failed to update customer verification", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return errors.New("failed to update customer verification")
}
s.logger.Info("Customer verification updated successfully", map[string]interface{}{
"customer_id": id.String(),
"customer_id": id,
"is_verified": form.IsVerified,
"is_compliant": form.IsCompliant,
"updated_by": updatedBy.String(),
"updated_by": updatedBy,
})
return nil
}
// CreateCustomerMobile creates a new customer via mobile app (simplified)
func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *uuid.UUID) (*Customer, error) {
func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) {
// Check if email already exists
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
if existingCustomer != nil {
@@ -583,7 +566,6 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
// Create customer with mobile form
customer := &Customer{
ID: uuid.New(),
Type: CustomerType(form.Type),
Status: CustomerStatusActive,
FirstName: form.FirstName,
@@ -601,8 +583,6 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
Language: "en",
Currency: "USD",
Timezone: "UTC",
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
CreatedBy: createdBy,
}
@@ -623,23 +603,23 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
"error": err.Error(),
"email": form.Email,
"type": form.Type,
"created_by": createdBy.String(),
"created_by": createdBy,
})
return nil, err
}
s.logger.Info("Customer created via mobile successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
"created_by": createdBy.String(),
"created_by": *createdBy,
})
return customer, nil
}
// UpdateCustomerMobile updates a customer via mobile app (simplified)
func (s *customerService) UpdateCustomerMobile(ctx context.Context, id uuid.UUID, form *UpdateCustomerMobileForm, updatedBy *uuid.UUID) (*Customer, error) {
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 {
@@ -683,22 +663,22 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id uuid.UUID
if err != nil {
s.logger.Error("Failed to update customer via mobile", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"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.String(),
"customer_id": customer.ID,
"email": customer.Email,
"updated_by": updatedBy.String(),
"updated_by": *updatedBy,
})
return customer, nil
}
// VerifyCustomer verifies a customer
func (s *customerService) VerifyCustomer(ctx context.Context, id uuid.UUID, verifiedBy *uuid.UUID) error {
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 {
@@ -710,21 +690,21 @@ func (s *customerService) VerifyCustomer(ctx context.Context, id uuid.UUID, veri
if err != nil {
s.logger.Error("Failed to verify customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return errors.New("failed to verify customer")
}
s.logger.Info("Customer verified successfully", map[string]interface{}{
"customer_id": id.String(),
"verified_by": verifiedBy.String(),
"customer_id": id,
"verified_by": verifiedBy,
})
return nil
}
// SuspendCustomer suspends a customer
func (s *customerService) SuspendCustomer(ctx context.Context, id uuid.UUID, reason string, suspendedBy *uuid.UUID) error {
func (s *customerService) SuspendCustomer(ctx context.Context, id string, reason string, suspendedBy *string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -736,22 +716,22 @@ func (s *customerService) SuspendCustomer(ctx context.Context, id uuid.UUID, rea
if err != nil {
s.logger.Error("Failed to suspend customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return errors.New("failed to suspend customer")
}
s.logger.Info("Customer suspended successfully", map[string]interface{}{
"customer_id": id.String(),
"customer_id": id,
"reason": reason,
"suspended_by": suspendedBy.String(),
"suspended_by": suspendedBy,
})
return nil
}
// ActivateCustomer activates a customer
func (s *customerService) ActivateCustomer(ctx context.Context, id uuid.UUID, activatedBy *uuid.UUID) error {
func (s *customerService) ActivateCustomer(ctx context.Context, id string, activatedBy *string) error {
// Get customer to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
@@ -763,14 +743,14 @@ func (s *customerService) ActivateCustomer(ctx context.Context, id uuid.UUID, ac
if err != nil {
s.logger.Error("Failed to activate customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"customer_id": id,
})
return errors.New("failed to activate customer")
}
s.logger.Info("Customer activated successfully", map[string]interface{}{
"customer_id": id.String(),
"activated_by": activatedBy.String(),
"customer_id": id,
"activated_by": activatedBy,
})
return nil
@@ -795,7 +775,7 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
if customer.Status != CustomerStatusActive {
s.logger.Warn("Customer login failed - account not active", map[string]interface{}{
"username": form.Username,
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"status": string(customer.Status),
})
return nil, errors.New("account is not active")
@@ -805,7 +785,7 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password))
if err != nil {
s.logger.Warn("Customer login failed - wrong password", map[string]interface{}{
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"email": customer.Email,
})
return nil, errors.New("invalid credentials")
@@ -814,11 +794,11 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
// Generate JWT tokens using authorization service
companyID := ""
if customer.CompanyID != nil {
companyID = customer.CompanyID.String()
companyID = *customer.CompanyID
}
tokenPair, err := s.authService.GenerateTokenPair(
customer.ID.String(),
customer.ID,
customer.Email,
"customer", // Role for customers
companyID,
@@ -826,14 +806,14 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
if err != nil {
s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
})
return nil, errors.New("failed to generate authentication tokens")
}
s.logger.Info("Customer login successful", map[string]interface{}{
"username": form.Username,
"customer_id": customer.ID.String(),
"customer_id": customer.ID,
"customer_type": string(customer.Type),
})
@@ -885,7 +865,7 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
return nil, errors.New("invalid token format")
}
customer, err := s.repository.GetByID(ctx, customerID)
customer, err := s.repository.GetByID(ctx, customerID.String())
if err != nil {
s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{
"error": err.Error(),
@@ -900,8 +880,7 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
}
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
"customer_id": customer.ID,
"customer_type": string(customer.Type),
})
@@ -914,22 +893,22 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
}
// GetProfile retrieves customer profile information
func (s *customerService) GetProfile(ctx context.Context, customerID uuid.UUID) (*Customer, error) {
func (s *customerService) GetProfile(ctx context.Context, customerID string) (*Customer, error) {
s.logger.Info("Getting customer profile", map[string]interface{}{
"customer_id": customerID.String(),
"customer_id": customerID,
})
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer profile", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
s.logger.Info("Customer profile retrieved successfully", map[string]interface{}{
"customer_id": customerID.String(),
"customer_id": customerID,
"customer_type": string(customer.Type),
})
@@ -937,9 +916,9 @@ func (s *customerService) GetProfile(ctx context.Context, customerID uuid.UUID)
}
// Logout handles customer logout
func (s *customerService) Logout(ctx context.Context, customerID uuid.UUID, accessToken string) error {
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
s.logger.Info("Customer logout", map[string]interface{}{
"customer_id": customerID.String(),
"customer_id": customerID,
"access_token": accessToken[:10] + "...", // Log only first 10 chars for security
})
@@ -948,13 +927,13 @@ func (s *customerService) Logout(ctx context.Context, customerID uuid.UUID, acce
if err != nil {
s.logger.Error("Failed to invalidate access token", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"customer_id": customerID,
})
// Don't return error here as the customer should still be logged out
}
s.logger.Info("Customer logout successful", map[string]interface{}{
"customer_id": customerID.String(),
"customer_id": customerID,
})
return nil