Refactor customer domain structure and implement core entities, services, and handlers

- Introduced a flat structure for the customer domain, consolidating entities, aggregates, repositories, services, forms, and handlers into a single package.
- Added core entities: Customer and CustomerAggregate.
- Implemented request/response forms for customer authentication (Register, Login, Refresh Token, Change Password).
- Created CustomerService to handle business logic and data access through the Repository interface.
- Established CustomerHandler for HTTP request handling related to customer authentication.
- Removed previous domain structure artifacts to streamline the codebase.
This commit is contained in:
n.nakhostin
2025-08-02 11:26:35 +03:30
parent ddf29efb4a
commit 3e9877574c
14 changed files with 337 additions and 1327 deletions
+24 -512
View File
@@ -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
- **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