diff --git a/.cursor/rules/backend-coding-rules.mdc b/.cursor/rules/backend-coding-rules.mdc index c59ed8b..2cd10c5 100644 --- a/.cursor/rules/backend-coding-rules.mdc +++ b/.cursor/rules/backend-coding-rules.mdc @@ -1,7 +1,5 @@ --- -description: -globs: -alwaysApply: false +alwaysApply: true --- # Cursor Rules for Tender Management Go Backend @@ -18,32 +16,25 @@ This is a Go backend API for a tender management system following Clean Architec - **Infrastructure Layer** (`infra/`): External dependencies (DB, cache, queues, config) ### Domain Structure Pattern -Each domain follows this structure: +Each domain follows this flat structure: ``` internal/{domain}/ -├── domain/ -│ ├── entity/ -│ │ └── entity.go # Core domain entities -│ ├── aggregate/ -│ │ └── aggregate.go # Aggregate roots and DTOs -│ ├── {domain}.go # Repository implementation -│ └── repository.go # Repository interface -├── service/ -│ ├── {domain}.go # Main service implementation -│ ├── profile.go # Profile-related operations -│ └── form.go # Request/response forms -└── handler/ - ├── v1/ - │ └── handler.go # HTTP handlers - └── middleware/ # Domain-specific middleware +├── entity.go # Core domain entities +├── aggregate.go # Aggregate roots and DTOs +├── repository.go # Repository interface and implementation +├── service.go # Business logic and use cases +├── form.go # Request/response forms with validation +└── handler.go # HTTP controllers and request/response handling ``` +**Package Structure**: All files in a domain use the same package name (e.g., `package customer`) + ### Dependency Rules - Always depend on interfaces, not concrete implementations - Use dependency injection through constructors -- Inner layers should never depend on outer layers -- All dependencies flow inward toward the domain -- Repository implementations are embedded in domain package +- All files in a domain use the same package name for easy access +- Repository implementations are in the same package as interfaces +- Keep dependencies minimal and focused ## 💻 Go Best Practices @@ -77,17 +68,19 @@ internal/{domain}/ ## 🔌 Interface Design ### Repository Interfaces -- Always create interfaces in the domain layer (`repository.go`) +- Always create interfaces in the same package (`repository.go`) - Use context.Context as first parameter for all methods - Return domain entities, not database models - Include common operations: Create, GetByID, Update, Delete, List - Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`) +- Repository implementation is in the same file as the interface ### Service Interfaces -- Define business operations clearly in service package +- Define business operations clearly in service.go - Use request/response forms for complex operations - Include validation in service methods - Handle business logic, not infrastructure concerns +- Service implementation is in the same package as other domain files ## 📝 Logging Standards @@ -319,15 +312,15 @@ log.Info("Customer login") ## 📋 Checklist for New Features ### Before Implementation -- [ ] Define interfaces in domain layer -- [ ] Create proper request/response forms with govalidator tags +- [ ] Define interfaces in the same package (`repository.go`) +- [ ] Create proper request/response forms with govalidator tags (`form.go`) - [ ] Plan error handling strategy - [ ] Design database schema if needed - [ ] Consider caching requirements - [ ] Define custom validators if needed ### During Implementation -- [ ] Follow clean architecture layers +- [ ] Follow flat domain structure (all files in same package) - [ ] Use dependency injection - [ ] Implement proper logging - [ ] Add govalidator validation tags to forms @@ -342,502 +335,21 @@ log.Info("Customer login") - [ ] Check performance impact - [ ] Test validation scenarios -## 🎯 Code Examples - -### Time Handling Utilities -```go -// pkg/timeutil/timeutil.go -package timeutil - -import "time" - -// NowUnix returns current Unix timestamp -func NowUnix() int64 { - return time.Now().Unix() -} - -// ValidateUnixTimestamp validates if Unix timestamp is reasonable -func ValidateUnixTimestamp(unix int64) bool { - // Check if timestamp is not too old (before 2020) or too future (after 2030) - minTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix() - maxTime := time.Date(2030, 12, 31, 23, 59, 59, 0, time.UTC).Unix() - return unix >= minTime && unix <= maxTime -} - -// ParseUnixTimestamp parses string to Unix timestamp -func ParseUnixTimestamp(timestamp string) (int64, error) { - // Handle various timestamp formats and convert to Unix - // This can be extended based on your input requirements - return 0, nil // Implementation depends on input format -} - -// FormatUnixTimestamp formats Unix timestamp for display (if needed) -func FormatUnixTimestamp(unix int64, format string) string { - return time.Unix(unix, 0).Format(format) -} -``` - -### Govalidator Request Forms -```go -// internal/customer/service/form.go -package service - -import ( - "github.com/asaskevich/govalidator" - "github.com/google/uuid" -) - -// RegisterForm represents customer registration request -type RegisterForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required,length(8|128)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - CompanyID string `json:"company_id" valid:"required,uuid"` -} - -// LoginForm represents customer login request -type LoginForm struct { - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required"` -} - -// UpdateProfileForm represents profile update request -type UpdateProfileForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"` -} - -// CreateCustomerForm represents customer creation request -type CreateCustomerForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required,length(8|128)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - CompanyID string `json:"company_id" valid:"required,uuid"` - CreatedAt int64 `json:"created_at,omitempty" valid:"optional,unix_timestamp"` - UpdatedAt int64 `json:"updated_at,omitempty" valid:"optional,unix_timestamp"` - LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"` -} - -// Custom validation functions -func init() { - // Register custom validators - govalidator.CustomTypeTagMap.Set("unix_timestamp", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool { - if unix, ok := i.(int64); ok { - return timeutil.ValidateUnixTimestamp(unix) - } - return false - })) -} -``` - -### Domain Entity Pattern -```go -// internal/customer/domain/entity/entity.go -type Customer struct { - ID uuid.UUID `bson:"_id"` - Email string `bson:"email"` - Password string `bson:"password"` // Never serialize - FirstName string `bson:"first_name"` - LastName string `bson:"last_name"` - Mobile string `bson:"mobile"` - CompanyID uuid.UUID `bson:"company_id"` - IsActive bool `bson:"is_active"` - IsVerified bool `bson:"is_verified"` - DeviceTokens []string `bson:"device_tokens"` - LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp - CreatedAt int64 `bson:"created_at"` // Unix timestamp - UpdatedAt int64 `bson:"updated_at"` // Unix timestamp -} -``` - -### Aggregate Pattern -```go -// internal/customer/domain/aggregate/aggregate.go -type Customer struct { - ID uuid.UUID `json:"_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Mobile string `json:"mobile"` - Email string `json:"email"` - IsActive bool `json:"is_active"` - IsVerified bool `json:"is_verified"` - LastLoginAt *int64 `json:"last_login_at,omitempty"` // Unix timestamp - CreatedAt int64 `json:"created_at"` // Unix timestamp - UpdatedAt int64 `json:"updated_at"` // Unix timestamp -} - -func NewCustomer(c entity.Customer) Customer { - customer := Customer{ - ID: c.ID, - FirstName: c.FirstName, - LastName: c.LastName, - Mobile: c.Mobile, - Email: c.Email, - IsActive: c.IsActive, - IsVerified: c.IsVerified, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, - } - - if c.LastLoginAt != nil { - customer.LastLoginAt = c.LastLoginAt - } - - return customer -} -``` - -### Repository Implementation with Time Handling -```go -// internal/customer/domain/customer.go -func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error { - // Set created/updated timestamps using Unix timestamps - now := time.Now().Unix() - customer.CreatedAt = now - customer.UpdatedAt = now - - // Insert customer - _, err := r.collection.InsertOne(ctx, customer) - if err != nil { - if mongo.IsDuplicateKeyError(err) { - return errors.New("customer already exists") - } - r.logger.Error("Failed to create customer", map[string]interface{}{ - "error": err.Error(), - "email": customer.Email, - "customer_id": customer.ID.String(), - }) - return err - } - - r.logger.Info("Customer created successfully", map[string]interface{}{ - "customer_id": customer.ID.String(), - "email": customer.Email, - }) - - return nil -} - -func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error { - // Set updated timestamp using Unix timestamp - customer.UpdatedAt = time.Now().Unix() - - filter := bson.M{"_id": customer.ID} - update := bson.M{"$set": customer} - - result, err := r.collection.UpdateOne(ctx, filter, update) - if err != nil { - r.logger.Error("Failed to update customer", map[string]interface{}{ - "error": err.Error(), - "customer_id": customer.ID.String(), - }) - return err - } - - if result.MatchedCount == 0 { - return errors.New("customer not found") - } - - return nil -} - -func (r *customerRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { - now := time.Now().Unix() - - filter := bson.M{"_id": id} - update := bson.M{ - "$set": bson.M{ - "last_login_at": now, - "updated_at": now, - }, - } - - result, err := r.collection.UpdateOne(ctx, filter, update) - if err != nil { - r.logger.Error("Failed to update last login", map[string]interface{}{ - "error": err.Error(), - "customer_id": id.String(), - }) - return err - } - - if result.MatchedCount == 0 { - return errors.New("customer not found") - } - - return nil -} -``` - -### Repository Interface -```go -// internal/customer/domain/repository.go -type Repository interface { - Create(ctx context.Context, customer *entity.Customer) error - GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) - GetByEmail(ctx context.Context, email string) (*entity.Customer, error) - Update(ctx context.Context, customer *entity.Customer) error - Delete(ctx context.Context, id uuid.UUID) error - List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) - GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) - AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error - RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error -} -``` - -### Service Implementation Template -```go -type CustomerService struct { - customerRepo domain.Repository - config *infra.AuthConfig - logger logger.Logger -} - -func NewCustomerService( - customerRepo domain.Repository, - config *infra.AuthConfig, - logger logger.Logger, -) CustomerService { - return CustomerService{ - customerRepo: customerRepo, - config: config, - logger: logger, - } -} - -func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.AuthorizationResponse, error) { - s.logger.Info("Processing customer registration", map[string]interface{}{ - "email": req.Email, - }) - - // Convert form to entity - customer := &entity.Customer{ - ID: uuid.New(), - FirstName: req.FirstName, - LastName: req.LastName, - Email: req.Email, - Password: req.Password, // Will be hashed in service - Mobile: req.Mobile, - CompanyID: uuid.MustParse(req.CompanyID), - IsActive: true, - IsVerified: false, - } - - // Set timestamps - s.setTimestamps(customer) - - // Create customer - if err := s.customerRepo.Create(ctx, customer); err != nil { - s.logger.Error("Failed to create customer", map[string]interface{}{ - "error": err.Error(), - "email": req.Email, - }) - return nil, err - } - - // Generate tokens and return response - // ... token generation logic - - return &aggregate.AuthorizationResponse{ - Customer: aggregate.NewCustomer(*customer), - // ... token fields - }, nil -} - -func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*entity.Customer, error) { - s.logger.Info("Retrieving customers", map[string]interface{}{ - "limit": limit, - "offset": offset, - }) - - customers, err := s.customerRepo.List(ctx, limit, offset) - if err != nil { - s.logger.Error("Failed to retrieve customers", map[string]interface{}{ - "error": err.Error(), - "limit": limit, - "offset": offset, - }) - return nil, errors.New("failed to retrieve customers") - } - - s.logger.Info("Customers retrieved successfully", map[string]interface{}{ - "count": len(customers), - "limit": limit, - "offset": offset, - }) - - return customers, nil -} - -// Time handling helper functions - all using Unix timestamps -func (s *CustomerService) setTimestamps(customer *entity.Customer) { - now := timeutil.NowUnix() - if customer.CreatedAt == 0 { - customer.CreatedAt = now - } - customer.UpdatedAt = now -} - -func (s *CustomerService) setLastLogin(customer *entity.Customer) { - now := timeutil.NowUnix() - customer.LastLoginAt = &now - customer.UpdatedAt = now -} - -func (s *CustomerService) validateTimestamps(customer *entity.Customer) error { - if customer.CreatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.CreatedAt) { - return errors.New("invalid created_at timestamp") - } - if customer.UpdatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.UpdatedAt) { - return errors.New("invalid updated_at timestamp") - } - if customer.LastLoginAt != nil && !timeutil.ValidateUnixTimestamp(*customer.LastLoginAt) { - return errors.New("invalid last_login_at timestamp") - } - return nil -} -``` - -### Handler Implementation Template -```go -type CustomerHandler struct { - service service.CustomerService - logger logger.Logger -} - -func NewHandler( - customerService service.CustomerService, - logger logger.Logger, -) *CustomerHandler { - return &CustomerHandler{ - service: customerService, - logger: logger, - } -} - -func (h *CustomerHandler) Register(c echo.Context) error { - var req service.RegisterForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - authResponse, err := h.service.Register(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Registration failed") - } - - return response.Created(c, authResponse, "Customer registered successfully") -} - -func (h *CustomerHandler) UpdateProfile(c echo.Context) error { - var req service.UpdateProfileForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - customer, err := h.service.UpdateProfile(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Profile update failed") - } - - return response.Success(c, customer, "Profile updated successfully") -} - -func (h *CustomerHandler) CreateWithTimestamp(c echo.Context) error { - var req service.CreateCustomerForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - customer, err := h.service.CreateCustomer(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Customer creation failed") - } - - return response.Created(c, customer, "Customer created successfully") -} -``` - -### Configuration Structure -```go -// infra/config.go -type Config struct { - Server ServerConfig `mapstructure:"server"` - Database DatabaseConfig `mapstructure:"database"` - Cache CacheConfig `mapstructure:"cache"` - Queue QueueConfig `mapstructure:"queue"` - Search SearchConfig `mapstructure:"search"` - Auth AuthConfig `mapstructure:"auth"` - AI AIConfig `mapstructure:"ai"` - Scraping ScrapingConfig `mapstructure:"scraping"` - Logging LoggingConfig `mapstructure:"logging"` - RateLimit RateLimitConfig `mapstructure:"rate_limiting"` -} -``` - -### Application Bootstrap -```go -// cmd/web/main.go -func main() { - conf := intiConfig() - - logger := initLogger(conf.Logging) - defer logger.Sync() - - logger.Info( - "Starting Tender Management API Server", - map[string]interface{}{ - "version": "1.0.0", - "host": conf.Server.Host, - "port": conf.Server.Port, - }) -} -``` - ## 🚀 Remember - Prioritize simplicity and readability over cleverness - Make code self-documenting through clear naming - Always consider the person who will maintain this code - Test your code thoroughly - Security and performance are not optional -- Follow the established domain structure pattern +- **Follow the flat domain structure**: All files in a domain use the same package name - Use the custom logger interface consistently - Implement proper error handling and logging -- Follow the repository pattern with embedded implementations +- Follow the repository pattern with interface and implementation in same file - **Time Consistency**: Always use Unix timestamps (int64) for input, storage, and output - **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges - **Time Utilities**: Use centralized time utility functions for consistency across the application - **No Format Conversion**: Never convert between Unix timestamps and other time formats in the application - **Validation**: Always use govalidator for request validation with proper tags - **Form Design**: Define validation rules in request forms, not in handlers -- **Custom Validators**: Register custom validators for complex validation rules \ No newline at end of file +- **Custom Validators**: Register custom validators for complex validation rules +- **Package Structure**: Keep all domain files in the same package for easy access and minimal imports \ No newline at end of file diff --git a/.cursorrules b/.cursorrules index 108b378..fd2b48c 100644 --- a/.cursorrules +++ b/.cursorrules @@ -13,32 +13,25 @@ This is a Go backend API for a tender management system following Clean Architec - **Infrastructure Layer** (`infra/`): External dependencies (DB, cache, queues, config) ### Domain Structure Pattern -Each domain follows this structure: +Each domain follows this flat structure: ``` internal/{domain}/ -├── domain/ -│ ├── entity/ -│ │ └── entity.go # Core domain entities -│ ├── aggregate/ -│ │ └── aggregate.go # Aggregate roots and DTOs -│ ├── {domain}.go # Repository implementation -│ └── repository.go # Repository interface -├── service/ -│ ├── {domain}.go # Main service implementation -│ ├── profile.go # Profile-related operations -│ └── form.go # Request/response forms -└── handler/ - ├── v1/ - │ └── handler.go # HTTP handlers - └── middleware/ # Domain-specific middleware +├── entity.go # Core domain entities +├── aggregate.go # Aggregate roots and DTOs +├── repository.go # Repository interface and implementation +├── service.go # Business logic and use cases +├── form.go # Request/response forms with validation +└── handler.go # HTTP controllers and request/response handling ``` +**Package Structure**: All files in a domain use the same package name (e.g., `package customer`) + ### Dependency Rules - Always depend on interfaces, not concrete implementations - Use dependency injection through constructors -- Inner layers should never depend on outer layers -- All dependencies flow inward toward the domain -- Repository implementations are embedded in domain package +- All files in a domain use the same package name for easy access +- Repository implementations are in the same package as interfaces +- Keep dependencies minimal and focused ## 💻 Go Best Practices @@ -72,17 +65,19 @@ internal/{domain}/ ## 🔌 Interface Design ### Repository Interfaces -- Always create interfaces in the domain layer (`repository.go`) +- Always create interfaces in the same package (`repository.go`) - Use context.Context as first parameter for all methods - Return domain entities, not database models - Include common operations: Create, GetByID, Update, Delete, List - Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`) +- Repository implementation is in the same file as the interface ### Service Interfaces -- Define business operations clearly in service package +- Define business operations clearly in service.go - Use request/response forms for complex operations - Include validation in service methods - Handle business logic, not infrastructure concerns +- Service implementation is in the same package as other domain files ## 📝 Logging Standards @@ -314,15 +309,15 @@ log.Info("Customer login") ## 📋 Checklist for New Features ### Before Implementation -- [ ] Define interfaces in domain layer -- [ ] Create proper request/response forms with govalidator tags +- [ ] Define interfaces in the same package (`repository.go`) +- [ ] Create proper request/response forms with govalidator tags (`form.go`) - [ ] Plan error handling strategy - [ ] Design database schema if needed - [ ] Consider caching requirements - [ ] Define custom validators if needed ### During Implementation -- [ ] Follow clean architecture layers +- [ ] Follow flat domain structure (all files in same package) - [ ] Use dependency injection - [ ] Implement proper logging - [ ] Add govalidator validation tags to forms @@ -337,502 +332,21 @@ log.Info("Customer login") - [ ] Check performance impact - [ ] Test validation scenarios -## 🎯 Code Examples - -### Time Handling Utilities -```go -// pkg/timeutil/timeutil.go -package timeutil - -import "time" - -// NowUnix returns current Unix timestamp -func NowUnix() int64 { - return time.Now().Unix() -} - -// ValidateUnixTimestamp validates if Unix timestamp is reasonable -func ValidateUnixTimestamp(unix int64) bool { - // Check if timestamp is not too old (before 2020) or too future (after 2030) - minTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix() - maxTime := time.Date(2030, 12, 31, 23, 59, 59, 0, time.UTC).Unix() - return unix >= minTime && unix <= maxTime -} - -// ParseUnixTimestamp parses string to Unix timestamp -func ParseUnixTimestamp(timestamp string) (int64, error) { - // Handle various timestamp formats and convert to Unix - // This can be extended based on your input requirements - return 0, nil // Implementation depends on input format -} - -// FormatUnixTimestamp formats Unix timestamp for display (if needed) -func FormatUnixTimestamp(unix int64, format string) string { - return time.Unix(unix, 0).Format(format) -} -``` - -### Govalidator Request Forms -```go -// internal/customer/service/form.go -package service - -import ( - "github.com/asaskevich/govalidator" - "github.com/google/uuid" -) - -// RegisterForm represents customer registration request -type RegisterForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required,length(8|128)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - CompanyID string `json:"company_id" valid:"required,uuid"` -} - -// LoginForm represents customer login request -type LoginForm struct { - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required"` -} - -// UpdateProfileForm represents profile update request -type UpdateProfileForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"` -} - -// CreateCustomerForm represents customer creation request -type CreateCustomerForm struct { - FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` - LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` - Email string `json:"email" valid:"required,email"` - Password string `json:"password" valid:"required,length(8|128)"` - Mobile string `json:"mobile" valid:"required,length(10|15)"` - CompanyID string `json:"company_id" valid:"required,uuid"` - CreatedAt int64 `json:"created_at,omitempty" valid:"optional,unix_timestamp"` - UpdatedAt int64 `json:"updated_at,omitempty" valid:"optional,unix_timestamp"` - LastLoginAt *int64 `json:"last_login_at,omitempty" valid:"optional,unix_timestamp"` -} - -// Custom validation functions -func init() { - // Register custom validators - govalidator.CustomTypeTagMap.Set("unix_timestamp", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool { - if unix, ok := i.(int64); ok { - return timeutil.ValidateUnixTimestamp(unix) - } - return false - })) -} -``` - -### Domain Entity Pattern -```go -// internal/customer/domain/entity/entity.go -type Customer struct { - ID uuid.UUID `bson:"_id"` - Email string `bson:"email"` - Password string `bson:"password"` // Never serialize - FirstName string `bson:"first_name"` - LastName string `bson:"last_name"` - Mobile string `bson:"mobile"` - CompanyID uuid.UUID `bson:"company_id"` - IsActive bool `bson:"is_active"` - IsVerified bool `bson:"is_verified"` - DeviceTokens []string `bson:"device_tokens"` - LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp - CreatedAt int64 `bson:"created_at"` // Unix timestamp - UpdatedAt int64 `bson:"updated_at"` // Unix timestamp -} -``` - -### Aggregate Pattern -```go -// internal/customer/domain/aggregate/aggregate.go -type Customer struct { - ID uuid.UUID `json:"_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Mobile string `json:"mobile"` - Email string `json:"email"` - IsActive bool `json:"is_active"` - IsVerified bool `json:"is_verified"` - LastLoginAt *int64 `json:"last_login_at,omitempty"` // Unix timestamp - CreatedAt int64 `json:"created_at"` // Unix timestamp - UpdatedAt int64 `json:"updated_at"` // Unix timestamp -} - -func NewCustomer(c entity.Customer) Customer { - customer := Customer{ - ID: c.ID, - FirstName: c.FirstName, - LastName: c.LastName, - Mobile: c.Mobile, - Email: c.Email, - IsActive: c.IsActive, - IsVerified: c.IsVerified, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, - } - - if c.LastLoginAt != nil { - customer.LastLoginAt = c.LastLoginAt - } - - return customer -} -``` - -### Repository Implementation with Time Handling -```go -// internal/customer/domain/customer.go -func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error { - // Set created/updated timestamps using Unix timestamps - now := time.Now().Unix() - customer.CreatedAt = now - customer.UpdatedAt = now - - // Insert customer - _, err := r.collection.InsertOne(ctx, customer) - if err != nil { - if mongo.IsDuplicateKeyError(err) { - return errors.New("customer already exists") - } - r.logger.Error("Failed to create customer", map[string]interface{}{ - "error": err.Error(), - "email": customer.Email, - "customer_id": customer.ID.String(), - }) - return err - } - - r.logger.Info("Customer created successfully", map[string]interface{}{ - "customer_id": customer.ID.String(), - "email": customer.Email, - }) - - return nil -} - -func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error { - // Set updated timestamp using Unix timestamp - customer.UpdatedAt = time.Now().Unix() - - filter := bson.M{"_id": customer.ID} - update := bson.M{"$set": customer} - - result, err := r.collection.UpdateOne(ctx, filter, update) - if err != nil { - r.logger.Error("Failed to update customer", map[string]interface{}{ - "error": err.Error(), - "customer_id": customer.ID.String(), - }) - return err - } - - if result.MatchedCount == 0 { - return errors.New("customer not found") - } - - return nil -} - -func (r *customerRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { - now := time.Now().Unix() - - filter := bson.M{"_id": id} - update := bson.M{ - "$set": bson.M{ - "last_login_at": now, - "updated_at": now, - }, - } - - result, err := r.collection.UpdateOne(ctx, filter, update) - if err != nil { - r.logger.Error("Failed to update last login", map[string]interface{}{ - "error": err.Error(), - "customer_id": id.String(), - }) - return err - } - - if result.MatchedCount == 0 { - return errors.New("customer not found") - } - - return nil -} -``` - -### Repository Interface -```go -// internal/customer/domain/repository.go -type Repository interface { - Create(ctx context.Context, customer *entity.Customer) error - GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) - GetByEmail(ctx context.Context, email string) (*entity.Customer, error) - Update(ctx context.Context, customer *entity.Customer) error - Delete(ctx context.Context, id uuid.UUID) error - List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) - GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) - AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error - RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error -} -``` - -### Service Implementation Template -```go -type CustomerService struct { - customerRepo domain.Repository - config *infra.AuthConfig - logger logger.Logger -} - -func NewCustomerService( - customerRepo domain.Repository, - config *infra.AuthConfig, - logger logger.Logger, -) CustomerService { - return CustomerService{ - customerRepo: customerRepo, - config: config, - logger: logger, - } -} - -func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.AuthorizationResponse, error) { - s.logger.Info("Processing customer registration", map[string]interface{}{ - "email": req.Email, - }) - - // Convert form to entity - customer := &entity.Customer{ - ID: uuid.New(), - FirstName: req.FirstName, - LastName: req.LastName, - Email: req.Email, - Password: req.Password, // Will be hashed in service - Mobile: req.Mobile, - CompanyID: uuid.MustParse(req.CompanyID), - IsActive: true, - IsVerified: false, - } - - // Set timestamps - s.setTimestamps(customer) - - // Create customer - if err := s.customerRepo.Create(ctx, customer); err != nil { - s.logger.Error("Failed to create customer", map[string]interface{}{ - "error": err.Error(), - "email": req.Email, - }) - return nil, err - } - - // Generate tokens and return response - // ... token generation logic - - return &aggregate.AuthorizationResponse{ - Customer: aggregate.NewCustomer(*customer), - // ... token fields - }, nil -} - -func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*entity.Customer, error) { - s.logger.Info("Retrieving customers", map[string]interface{}{ - "limit": limit, - "offset": offset, - }) - - customers, err := s.customerRepo.List(ctx, limit, offset) - if err != nil { - s.logger.Error("Failed to retrieve customers", map[string]interface{}{ - "error": err.Error(), - "limit": limit, - "offset": offset, - }) - return nil, errors.New("failed to retrieve customers") - } - - s.logger.Info("Customers retrieved successfully", map[string]interface{}{ - "count": len(customers), - "limit": limit, - "offset": offset, - }) - - return customers, nil -} - -// Time handling helper functions - all using Unix timestamps -func (s *CustomerService) setTimestamps(customer *entity.Customer) { - now := timeutil.NowUnix() - if customer.CreatedAt == 0 { - customer.CreatedAt = now - } - customer.UpdatedAt = now -} - -func (s *CustomerService) setLastLogin(customer *entity.Customer) { - now := timeutil.NowUnix() - customer.LastLoginAt = &now - customer.UpdatedAt = now -} - -func (s *CustomerService) validateTimestamps(customer *entity.Customer) error { - if customer.CreatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.CreatedAt) { - return errors.New("invalid created_at timestamp") - } - if customer.UpdatedAt != 0 && !timeutil.ValidateUnixTimestamp(customer.UpdatedAt) { - return errors.New("invalid updated_at timestamp") - } - if customer.LastLoginAt != nil && !timeutil.ValidateUnixTimestamp(*customer.LastLoginAt) { - return errors.New("invalid last_login_at timestamp") - } - return nil -} -``` - -### Handler Implementation Template -```go -type CustomerHandler struct { - service service.CustomerService - logger logger.Logger -} - -func NewHandler( - customerService service.CustomerService, - logger logger.Logger, -) *CustomerHandler { - return &CustomerHandler{ - service: customerService, - logger: logger, - } -} - -func (h *CustomerHandler) Register(c echo.Context) error { - var req service.RegisterForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - authResponse, err := h.service.Register(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Registration failed") - } - - return response.Created(c, authResponse, "Customer registered successfully") -} - -func (h *CustomerHandler) UpdateProfile(c echo.Context) error { - var req service.UpdateProfileForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - customer, err := h.service.UpdateProfile(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Profile update failed") - } - - return response.Success(c, customer, "Profile updated successfully") -} - -func (h *CustomerHandler) CreateWithTimestamp(c echo.Context) error { - var req service.CreateCustomerForm - - // Bind request body - if err := c.Bind(&req); err != nil { - return response.BadRequest(c, "Invalid request format", err.Error()) - } - - // Validate request using govalidator - if _, err := govalidator.ValidateStruct(&req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - // Call service - customer, err := h.service.CreateCustomer(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Customer creation failed") - } - - return response.Created(c, customer, "Customer created successfully") -} -``` - -### Configuration Structure -```go -// infra/config.go -type Config struct { - Server ServerConfig `mapstructure:"server"` - Database DatabaseConfig `mapstructure:"database"` - Cache CacheConfig `mapstructure:"cache"` - Queue QueueConfig `mapstructure:"queue"` - Search SearchConfig `mapstructure:"search"` - Auth AuthConfig `mapstructure:"auth"` - AI AIConfig `mapstructure:"ai"` - Scraping ScrapingConfig `mapstructure:"scraping"` - Logging LoggingConfig `mapstructure:"logging"` - RateLimit RateLimitConfig `mapstructure:"rate_limiting"` -} -``` - -### Application Bootstrap -```go -// cmd/web/main.go -func main() { - conf := intiConfig() - - logger := initLogger(conf.Logging) - defer logger.Sync() - - logger.Info( - "Starting Tender Management API Server", - map[string]interface{}{ - "version": "1.0.0", - "host": conf.Server.Host, - "port": conf.Server.Port, - }) -} -``` - ## 🚀 Remember - Prioritize simplicity and readability over cleverness - Make code self-documenting through clear naming - Always consider the person who will maintain this code - Test your code thoroughly - Security and performance are not optional -- Follow the established domain structure pattern +- **Follow the flat domain structure**: All files in a domain use the same package name - Use the custom logger interface consistently - Implement proper error handling and logging -- Follow the repository pattern with embedded implementations +- Follow the repository pattern with interface and implementation in same file - **Time Consistency**: Always use Unix timestamps (int64) for input, storage, and output - **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges - **Time Utilities**: Use centralized time utility functions for consistency across the application - **No Format Conversion**: Never convert between Unix timestamps and other time formats in the application - **Validation**: Always use govalidator for request validation with proper tags - **Form Design**: Define validation rules in request forms, not in handlers -- **Custom Validators**: Register custom validators for complex validation rules \ No newline at end of file +- **Custom Validators**: Register custom validators for complex validation rules +- **Package Structure**: Keep all domain files in the same package for easy access and minimal imports \ No newline at end of file diff --git a/internal/customer/aggregate.go b/internal/customer/aggregate.go new file mode 100644 index 0000000..4b989d0 --- /dev/null +++ b/internal/customer/aggregate.go @@ -0,0 +1,46 @@ +package customer + +import ( + "github.com/google/uuid" +) + +type CustomerAggregate struct { + ID uuid.UUID `json:"_id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Mobile string `json:"mobile"` + Email string `json:"email"` + IsActive bool `json:"is_active"` + IsVerified bool `json:"is_verified"` + LastLoginAt *int64 `json:"last_login_at,omitempty"` // Unix timestamp + CreatedAt int64 `json:"created_at"` // Unix timestamp + UpdatedAt int64 `json:"updated_at"` // Unix timestamp +} + +func NewCustomerAggregate(c Customer) CustomerAggregate { + customer := CustomerAggregate{ + ID: c.ID, + FirstName: c.FirstName, + LastName: c.LastName, + Mobile: c.Mobile, + Email: c.Email, + IsActive: c.IsActive, + IsVerified: c.IsVerified, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } + + if c.LastLoginAt != nil { + customer.LastLoginAt = c.LastLoginAt + } + + return customer +} + +// AuthResponse defines authentication response (kept for backward compatibility) +type AuthorizationResponse struct { + Customer CustomerAggregate `json:"customer"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` +} diff --git a/internal/customer/domain/aggregate/aggregate.go b/internal/customer/domain/aggregate/aggregate.go deleted file mode 100644 index 81ed162..0000000 --- a/internal/customer/domain/aggregate/aggregate.go +++ /dev/null @@ -1,37 +0,0 @@ -package aggregate - -import ( - "tm/internal/customer/domain/entity" - - "github.com/google/uuid" -) - -type Customer struct { - ID uuid.UUID `json:"_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Mobile string `json:"mobile"` - Email string `json:"email"` - IsActive bool `json:"is_active"` - IsVerified bool `json:"is_verified"` -} - -func NewCustomer(c entity.Customer) Customer { - return Customer{ - ID: c.ID, - FirstName: c.FirstName, - LastName: c.LastName, - Mobile: c.Mobile, - Email: c.Email, - IsActive: c.IsActive, - IsVerified: c.IsVerified, - } -} - -// AuthResponse defines authentication response (kept for backward compatibility) -type AuthorizationResponse struct { - Customer Customer `json:"customer"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int64 `json:"expires_in"` -} diff --git a/internal/customer/domain/entity/entity.go b/internal/customer/domain/entity/entity.go deleted file mode 100644 index 7e895c4..0000000 --- a/internal/customer/domain/entity/entity.go +++ /dev/null @@ -1,24 +0,0 @@ -package entity - -import ( - "time" - - "github.com/google/uuid" -) - -// Customer represents a mobile app user (customer) -type Customer struct { - ID uuid.UUID `bson:"_id"` - Email string `bson:"email"` - Password string `bson:"password"` // Never serialize password - FirstName string `bson:"first_name"` - LastName string `bson:"last_name"` - Mobile string `bson:"mobile"` - CompanyID uuid.UUID `bson:"company_id"` - IsActive bool `bson:"is_active"` - IsVerified bool `bson:"is_verified"` - DeviceTokens []string `bson:"device_tokens"` // For push notifications - LastLoginAt *time.Time `bson:"last_login_at,omitempty"` - CreatedAt time.Time `bson:"created_at"` - UpdatedAt time.Time `bson:"updated_at"` -} diff --git a/internal/customer/domain/repository.go b/internal/customer/domain/repository.go deleted file mode 100644 index c86bb7e..0000000 --- a/internal/customer/domain/repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package customer - -import ( - "context" - "tm/internal/customer/domain/entity" - - "github.com/google/uuid" -) - -// CustomerRepository defines methods for customer (mobile user) data access -type Repository interface { - Create(ctx context.Context, customer *entity.Customer) error - GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) - GetByEmail(ctx context.Context, email string) (*entity.Customer, error) - Update(ctx context.Context, customer *entity.Customer) error - Delete(ctx context.Context, id uuid.UUID) error - List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) - GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) - AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error - RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error -} diff --git a/internal/customer/entity.go b/internal/customer/entity.go new file mode 100644 index 0000000..ceaed2c --- /dev/null +++ b/internal/customer/entity.go @@ -0,0 +1,22 @@ +package customer + +import ( + "github.com/google/uuid" +) + +// Customer represents a mobile app user (customer) +type Customer struct { + ID uuid.UUID `bson:"_id"` + Email string `bson:"email"` + Password string `bson:"password"` // Never serialize password + FirstName string `bson:"first_name"` + LastName string `bson:"last_name"` + Mobile string `bson:"mobile"` + CompanyID uuid.UUID `bson:"company_id"` + IsActive bool `bson:"is_active"` + IsVerified bool `bson:"is_verified"` + DeviceTokens []string `bson:"device_tokens"` // For push notifications + LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp + CreatedAt int64 `bson:"created_at"` // Unix timestamp + UpdatedAt int64 `bson:"updated_at"` // Unix timestamp +} diff --git a/internal/customer/form.go b/internal/customer/form.go new file mode 100644 index 0000000..11890cb --- /dev/null +++ b/internal/customer/form.go @@ -0,0 +1,24 @@ +package customer + +// Customer Authentication DTOs +type RegisterForm struct { + FirstName string `json:"first_name" valid:"required,alpha,length(2|50)"` + LastName string `json:"last_name" valid:"required,alpha,length(2|50)"` + Email string `json:"email" valid:"required,email"` + Mobile string `json:"mobile,omitempty" valid:"optional,length(10|15)"` + Password string `json:"password" valid:"required,length(8|128)"` +} + +type LoginForm struct { + Email string `json:"email" valid:"required,email"` + Password string `json:"password" valid:"required"` +} + +type RefreshTokenForm struct { + RefreshToken string `json:"refresh_token" valid:"required"` +} + +type ChangePasswordForm struct { + OldPassword string `json:"old_password" valid:"required"` + NewPassword string `json:"new_password" valid:"required,length(8|128)"` +} diff --git a/internal/customer/handler/v1/handler.go b/internal/customer/handler.go similarity index 96% rename from internal/customer/handler/v1/handler.go rename to internal/customer/handler.go index ba76d28..cb97d2e 100644 --- a/internal/customer/handler/v1/handler.go +++ b/internal/customer/handler.go @@ -1,7 +1,6 @@ -package v1 +package customer import ( - service "tm/internal/customer/service" "tm/pkg/logger" "tm/pkg/response" @@ -11,13 +10,13 @@ import ( // CustomerHandler handles authentication related HTTP requests for mobile customers type CustomerHandler struct { - service service.CustomerService + service CustomerService logger logger.Logger } // NewCustomerHandler creates a new customer authentication handler func NewHandler( - customerAuthService service.CustomerService, + customerAuthService CustomerService, logger logger.Logger, ) *CustomerHandler { return &CustomerHandler{ @@ -39,7 +38,7 @@ func NewHandler( // @Failure 500 {object} response.APIResponse // @Router /api/mobile/auth/register [post] func (h *CustomerHandler) Register(c echo.Context) error { - var req service.RegisterForm + var req RegisterForm // Bind request body if err := c.Bind(&req); err != nil { @@ -73,7 +72,7 @@ func (h *CustomerHandler) Register(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /api/mobile/auth/login [post] func (h *CustomerHandler) Login(c echo.Context) error { - var req service.LoginForm + var req LoginForm // Bind request body if err := c.Bind(&req); err != nil { @@ -107,7 +106,7 @@ func (h *CustomerHandler) Login(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /api/mobile/auth/refresh [post] func (h *CustomerHandler) RefreshToken(c echo.Context) error { - var req service.RefreshTokenForm + var req RefreshTokenForm // Bind request body if err := c.Bind(&req); err != nil { @@ -142,7 +141,7 @@ func (h *CustomerHandler) RefreshToken(c echo.Context) error { // @Failure 500 {object} response.APIResponse // @Router /api/mobile/auth/change-password [post] func (h *CustomerHandler) ChangePassword(c echo.Context) error { - var req service.ChangePasswordForm + var req ChangePasswordForm // Bind request body if err := c.Bind(&req); err != nil { diff --git a/internal/customer/domain/customer.go b/internal/customer/repository.go similarity index 85% rename from internal/customer/domain/customer.go rename to internal/customer/repository.go index 5f7a3a7..fe46b92 100644 --- a/internal/customer/domain/customer.go +++ b/internal/customer/repository.go @@ -4,7 +4,6 @@ import ( "context" "errors" "time" - "tm/internal/customer/domain/entity" "tm/pkg/logger" "github.com/google/uuid" @@ -13,6 +12,19 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +// Repository defines methods for customer (mobile user) data access +type Repository interface { + Create(ctx context.Context, customer *Customer) error + GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) + GetByEmail(ctx context.Context, email string) (*Customer, error) + Update(ctx context.Context, customer *Customer) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, limit, offset int) ([]*Customer, error) + GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) + AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error + RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error +} + // customerRepository implements the Repository interface type customerRepository struct { collection *mongo.Collection @@ -68,9 +80,9 @@ func NewCustomerRepository(db *mongo.Database, logger logger.Logger) Repository } // Create creates a new customer -func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error { - // Set created/updated timestamps - now := time.Now() +func (r *customerRepository) Create(ctx context.Context, customer *Customer) error { + // Set created/updated timestamps using Unix timestamps + now := time.Now().Unix() customer.CreatedAt = now customer.UpdatedAt = now @@ -97,8 +109,8 @@ func (r *customerRepository) Create(ctx context.Context, customer *entity.Custom } // GetByID retrieves a customer by ID -func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) { - var customer entity.Customer +func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) { + var customer Customer filter := bson.M{"_id": id} err := r.collection.FindOne(ctx, filter).Decode(&customer) @@ -118,8 +130,8 @@ func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entity } // GetByEmail retrieves a customer by email -func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*entity.Customer, error) { - var customer entity.Customer +func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) { + var customer Customer filter := bson.M{"email": email} err := r.collection.FindOne(ctx, filter).Decode(&customer) @@ -139,9 +151,9 @@ func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*ent } // Update updates a customer -func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error { - // Set updated timestamp - customer.UpdatedAt = time.Now() +func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { + // Set updated timestamp using Unix timestamp + customer.UpdatedAt = time.Now().Unix() filter := bson.M{"_id": customer.ID} update := bson.M{"$set": customer} @@ -173,7 +185,7 @@ func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error { update := bson.M{ "$set": bson.M{ "is_active": false, - "updated_at": time.Now(), + "updated_at": time.Now().Unix(), }, } @@ -198,8 +210,8 @@ 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) ([]*entity.Customer, error) { - var customers []*entity.Customer +func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) { + var customers []*Customer // Build options opts := options.Find() @@ -232,8 +244,8 @@ func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*en } // GetByCompanyID retrieves customers by company ID with pagination -func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) { - var customers []*entity.Customer +func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) { + var customers []*Customer // Build options opts := options.Find() @@ -275,7 +287,7 @@ func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, t filter := bson.M{"_id": id} update := bson.M{ "$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates - "$set": bson.M{"updated_at": time.Now()}, + "$set": bson.M{"updated_at": time.Now().Unix()}, } result, err := r.collection.UpdateOne(ctx, filter, update) @@ -305,7 +317,7 @@ func (r *customerRepository) RemoveDeviceToken(ctx context.Context, id uuid.UUID filter := bson.M{"_id": id} update := bson.M{ "$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token - "$set": bson.M{"updated_at": time.Now()}, + "$set": bson.M{"updated_at": time.Now().Unix()}, } result, err := r.collection.UpdateOne(ctx, filter, update) diff --git a/internal/customer/service/profile.go b/internal/customer/service.go similarity index 69% rename from internal/customer/service/profile.go rename to internal/customer/service.go index 87381ed..f79e2bd 100644 --- a/internal/customer/service/profile.go +++ b/internal/customer/service.go @@ -4,17 +4,155 @@ import ( "context" "errors" "time" + infrastructure "tm/infra" + "tm/pkg/logger" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" - - "tm/internal/customer/domain/aggregate" - "tm/internal/customer/domain/entity" ) +// CustomerService implements the CustomerService interface +type CustomerService struct { + customerRepo Repository + config *infrastructure.AuthConfig + logger logger.Logger +} + +// NewCustomerService creates a new customer service +func NewCustomerService( + customerRepo Repository, + config *infrastructure.AuthConfig, + logger logger.Logger, +) CustomerService { + return CustomerService{ + customerRepo: customerRepo, + config: config, + logger: logger, + } +} + +// GetCustomers retrieves customers with pagination (for panel users) +func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error) { + s.logger.Info("Retrieving customers", map[string]interface{}{ + "limit": limit, + "offset": offset, + }) + + customers, err := s.customerRepo.List(ctx, limit, offset) + if err != nil { + s.logger.Error("Failed to retrieve customers", map[string]interface{}{ + "error": err.Error(), + "limit": limit, + "offset": offset, + }) + return nil, errors.New("failed to retrieve customers") + } + + s.logger.Info("Customers retrieved successfully", map[string]interface{}{ + "count": len(customers), + "limit": limit, + "offset": offset, + }) + + return customers, nil +} + +// GetCustomerByID retrieves a customer by ID (for panel users) +func (s *CustomerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) { + s.logger.Info("Retrieving customer by ID", map[string]interface{}{ + "customer_id": id.String(), + }) + + customer, err := s.customerRepo.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to retrieve customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return nil, errors.New("customer not found") + } + + s.logger.Info("Customer retrieved successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + }) + + return customer, nil +} + +// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users) +func (s *CustomerService) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) { + s.logger.Info("Retrieving customers by company", map[string]interface{}{ + "company_id": companyID.String(), + "limit": limit, + "offset": offset, + }) + + customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset) + if err != nil { + s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID.String(), + "limit": limit, + "offset": offset, + }) + return nil, errors.New("failed to retrieve customers") + } + + s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{ + "count": len(customers), + "company_id": companyID.String(), + "limit": limit, + "offset": offset, + }) + + return customers, nil +} + +// UpdateCustomerStatus updates customer active status (for panel users) +func (s *CustomerService) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error { + s.logger.Info("Updating customer status", map[string]interface{}{ + "customer_id": customerID.String(), + "is_active": isActive, + "updated_by": updatedBy.String(), + }) + + // Get customer + customer, err := s.customerRepo.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Customer not found for status update", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID.String(), + }) + return errors.New("customer not found") + } + + // Update status + customer.IsActive = isActive + + // Save changes + if err := s.customerRepo.Update(ctx, customer); err != nil { + s.logger.Error("Failed to update customer status", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID.String(), + "is_active": isActive, + "updated_by": updatedBy.String(), + }) + return errors.New("failed to update customer status") + } + + s.logger.Info("Customer status updated successfully", map[string]interface{}{ + "customer_id": customerID.String(), + "is_active": isActive, + "updated_by": updatedBy.String(), + }) + + return nil +} + // Register creates a new customer account -func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggregate.AuthorizationResponse, error) { +func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*AuthorizationResponse, error) { s.logger.Info("Registering new customer", map[string]interface{}{ "email": req.Email, }) @@ -35,7 +173,7 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr } // Create customer - customer := &entity.Customer{ + customer := &Customer{ ID: uuid.New(), Email: req.Email, Password: hashedPassword, @@ -45,8 +183,8 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr IsActive: true, IsVerified: false, // Require email verification DeviceTokens: make([]string, 0), - UpdatedAt: time.Now(), - CreatedAt: time.Now(), + UpdatedAt: time.Now().Unix(), + CreatedAt: time.Now().Unix(), } if err := s.customerRepo.Create(ctx, customer); err != nil { @@ -85,8 +223,8 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr // TODO: Send verification email s.sendVerificationEmail(ctx, customer) - return &aggregate.AuthorizationResponse{ - Customer: aggregate.NewCustomer(*customer), + return &AuthorizationResponse{ + Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), @@ -94,7 +232,7 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr } // Login authenticates a customer and returns tokens -func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate.AuthorizationResponse, error) { +func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*AuthorizationResponse, error) { s.logger.Info("Customer login attempt", map[string]interface{}{ "email": req.Email, }) @@ -127,9 +265,9 @@ func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate. } // Update last login time - customer.LastLoginAt = &time.Time{} - *customer.LastLoginAt = time.Now() - customer.UpdatedAt = time.Now() + now := time.Now().Unix() + customer.LastLoginAt = &now + customer.UpdatedAt = now if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Warn("Failed to update customer last login", map[string]interface{}{ @@ -164,8 +302,8 @@ func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate. "is_verified": customer.IsVerified, }) - return &aggregate.AuthorizationResponse{ - Customer: aggregate.NewCustomer(*customer), + return &AuthorizationResponse{ + Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), @@ -173,7 +311,7 @@ func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate. } // RefreshToken generates new tokens using refresh token -func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) (*aggregate.AuthorizationResponse, error) { +func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthorizationResponse, error) { // Parse and validate refresh token token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { @@ -234,8 +372,8 @@ func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) return nil, errors.New("failed to generate refresh token") } - return &aggregate.AuthorizationResponse{ - Customer: aggregate.NewCustomer(*customer), + return &AuthorizationResponse{ + Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: newRefreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), @@ -243,7 +381,7 @@ func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) } // ValidateToken validates a JWT token and returns the customer -func (s *CustomerService) ValidateToken(ctx context.Context, tokenString string) (*entity.Customer, error) { +func (s *CustomerService) ValidateToken(ctx context.Context, tokenString string) (*Customer, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("invalid signing method") @@ -314,7 +452,7 @@ func (s *CustomerService) ChangePassword(ctx context.Context, customerID uuid.UU // Update password customer.Password = hashedPassword - customer.UpdatedAt = time.Now() + customer.UpdatedAt = time.Now().Unix() if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Error("Failed to update customer password", map[string]interface{}{ @@ -383,7 +521,7 @@ func (s *CustomerService) verifyPassword(password, hashedPassword string) bool { return err == nil } -func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string, error) { +func (s *CustomerService) generateAccessToken(customer *Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "email": customer.Email, @@ -398,7 +536,7 @@ func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string return token.SignedString([]byte(s.config.JWT.Secret)) } -func (s *CustomerService) generateRefreshToken(customer *entity.Customer) (string, error) { +func (s *CustomerService) generateRefreshToken(customer *Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "user_type": "customer", @@ -411,7 +549,7 @@ func (s *CustomerService) generateRefreshToken(customer *entity.Customer) (strin return token.SignedString([]byte(s.config.JWT.Secret)) } -func (s *CustomerService) sendVerificationEmail(ctx context.Context, customer *entity.Customer) { +func (s *CustomerService) sendVerificationEmail(ctx context.Context, customer *Customer) { // TODO: Implement email sending logic _ = ctx s.logger.Info("Verification email would be sent", map[string]interface{}{ diff --git a/internal/customer/service/customer.go b/internal/customer/service/customer.go deleted file mode 100644 index 1c23781..0000000 --- a/internal/customer/service/customer.go +++ /dev/null @@ -1,151 +0,0 @@ -package customer - -import ( - "context" - "errors" - infrastructure "tm/infra" - domain "tm/internal/customer/domain" - "tm/internal/customer/domain/entity" - "tm/pkg/logger" - - "github.com/google/uuid" -) - -// CustomerService implements the CustomerService interface -type CustomerService struct { - customerRepo domain.Repository - config *infrastructure.AuthConfig - logger logger.Logger -} - -// NewCustomerService creates a new customer service -func NewCustomerService( - customerRepo domain.Repository, - config *infrastructure.AuthConfig, - logger logger.Logger, -) CustomerService { - return CustomerService{ - customerRepo: customerRepo, - config: config, - logger: logger, - } -} - -// GetCustomers retrieves customers with pagination (for panel users) -func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*entity.Customer, error) { - s.logger.Info("Retrieving customers", map[string]interface{}{ - "limit": limit, - "offset": offset, - }) - - customers, err := s.customerRepo.List(ctx, limit, offset) - if err != nil { - s.logger.Error("Failed to retrieve customers", map[string]interface{}{ - "error": err.Error(), - "limit": limit, - "offset": offset, - }) - return nil, errors.New("failed to retrieve customers") - } - - s.logger.Info("Customers retrieved successfully", map[string]interface{}{ - "count": len(customers), - "limit": limit, - "offset": offset, - }) - - return customers, nil -} - -// GetCustomerByID retrieves a customer by ID (for panel users) -func (s *CustomerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) { - s.logger.Info("Retrieving customer by ID", map[string]interface{}{ - "customer_id": id.String(), - }) - - customer, err := s.customerRepo.GetByID(ctx, id) - if err != nil { - s.logger.Error("Failed to retrieve customer", map[string]interface{}{ - "error": err.Error(), - "customer_id": id.String(), - }) - return nil, errors.New("customer not found") - } - - s.logger.Info("Customer retrieved successfully", map[string]interface{}{ - "customer_id": customer.ID.String(), - "email": customer.Email, - }) - - return customer, nil -} - -// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users) -func (s *CustomerService) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) { - s.logger.Info("Retrieving customers by company", map[string]interface{}{ - "company_id": companyID.String(), - "limit": limit, - "offset": offset, - }) - - customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset) - if err != nil { - s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID.String(), - "limit": limit, - "offset": offset, - }) - return nil, errors.New("failed to retrieve customers") - } - - s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{ - "count": len(customers), - "company_id": companyID.String(), - "limit": limit, - "offset": offset, - }) - - return customers, nil -} - -// UpdateCustomerStatus updates customer active status (for panel users) -func (s *CustomerService) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error { - s.logger.Info("Updating customer status", map[string]interface{}{ - "customer_id": customerID.String(), - "is_active": isActive, - "updated_by": updatedBy.String(), - }) - - // Get customer - customer, err := s.customerRepo.GetByID(ctx, customerID) - if err != nil { - s.logger.Error("Customer not found for status update", map[string]interface{}{ - "error": err.Error(), - "customer_id": customerID.String(), - }) - return errors.New("customer not found") - } - - // Update status - customer.IsActive = isActive - - // Save changes - if err := s.customerRepo.Update(ctx, customer); err != nil { - s.logger.Error("Failed to update customer status", map[string]interface{}{ - "error": err.Error(), - "customer_id": customerID.String(), - "is_active": isActive, - "updated_by": updatedBy.String(), - }) - return errors.New("failed to update customer status") - } - - s.logger.Info("Customer status updated successfully", map[string]interface{}{ - "customer_id": customerID.String(), - "is_active": isActive, - "updated_by": updatedBy.String(), - }) - - return nil -} diff --git a/internal/customer/service/form.go b/internal/customer/service/form.go deleted file mode 100644 index 5d719ec..0000000 --- a/internal/customer/service/form.go +++ /dev/null @@ -1,24 +0,0 @@ -package customer - -// Customer Authentication DTOs -type RegisterForm struct { - FirstName string `json:"first_name" validate:"required"` - LastName string `json:"last_name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Mobile string `json:"mobile,omitempty"` - Password string `json:"password" validate:"required,min=8"` -} - -type LoginForm struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required"` -} - -type RefreshTokenForm struct { - RefreshToken string `json:"refresh_token" validate:"required"` -} - -type ChangePasswordForm struct { - OldPassword string `json:"old_password" validate:"required"` - NewPassword string `json:"new_password" validate:"required,min=8"` -} diff --git a/LOGGING_UPGRADE.md b/pkg/logger/LOGGING_UPGRADE.md similarity index 100% rename from LOGGING_UPGRADE.md rename to pkg/logger/LOGGING_UPGRADE.md