Fix customer domain structure and update cursor rules
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
---
|
---
|
||||||
description:
|
alwaysApply: true
|
||||||
globs:
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
---
|
||||||
# Cursor Rules for Tender Management Go Backend
|
# 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)
|
- **Infrastructure Layer** (`infra/`): External dependencies (DB, cache, queues, config)
|
||||||
|
|
||||||
### Domain Structure Pattern
|
### Domain Structure Pattern
|
||||||
Each domain follows this structure:
|
Each domain follows this flat structure:
|
||||||
```
|
```
|
||||||
internal/{domain}/
|
internal/{domain}/
|
||||||
├── domain/
|
├── entity.go # Core domain entities
|
||||||
│ ├── entity/
|
├── aggregate.go # Aggregate roots and DTOs
|
||||||
│ │ └── entity.go # Core domain entities
|
├── repository.go # Repository interface and implementation
|
||||||
│ ├── aggregate/
|
├── service.go # Business logic and use cases
|
||||||
│ │ └── aggregate.go # Aggregate roots and DTOs
|
├── form.go # Request/response forms with validation
|
||||||
│ ├── {domain}.go # Repository implementation
|
└── handler.go # HTTP controllers and request/response handling
|
||||||
│ └── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Package Structure**: All files in a domain use the same package name (e.g., `package customer`)
|
||||||
|
|
||||||
### Dependency Rules
|
### Dependency Rules
|
||||||
- Always depend on interfaces, not concrete implementations
|
- Always depend on interfaces, not concrete implementations
|
||||||
- Use dependency injection through constructors
|
- Use dependency injection through constructors
|
||||||
- Inner layers should never depend on outer layers
|
- All files in a domain use the same package name for easy access
|
||||||
- All dependencies flow inward toward the domain
|
- Repository implementations are in the same package as interfaces
|
||||||
- Repository implementations are embedded in domain package
|
- Keep dependencies minimal and focused
|
||||||
|
|
||||||
## 💻 Go Best Practices
|
## 💻 Go Best Practices
|
||||||
|
|
||||||
@@ -77,17 +68,19 @@ internal/{domain}/
|
|||||||
## 🔌 Interface Design
|
## 🔌 Interface Design
|
||||||
|
|
||||||
### Repository Interfaces
|
### 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
|
- Use context.Context as first parameter for all methods
|
||||||
- Return domain entities, not database models
|
- Return domain entities, not database models
|
||||||
- Include common operations: Create, GetByID, Update, Delete, List
|
- Include common operations: Create, GetByID, Update, Delete, List
|
||||||
- Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`)
|
- Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`)
|
||||||
|
- Repository implementation is in the same file as the interface
|
||||||
|
|
||||||
### Service Interfaces
|
### Service Interfaces
|
||||||
- Define business operations clearly in service package
|
- Define business operations clearly in service.go
|
||||||
- Use request/response forms for complex operations
|
- Use request/response forms for complex operations
|
||||||
- Include validation in service methods
|
- Include validation in service methods
|
||||||
- Handle business logic, not infrastructure concerns
|
- Handle business logic, not infrastructure concerns
|
||||||
|
- Service implementation is in the same package as other domain files
|
||||||
|
|
||||||
## 📝 Logging Standards
|
## 📝 Logging Standards
|
||||||
|
|
||||||
@@ -319,15 +312,15 @@ log.Info("Customer login")
|
|||||||
## 📋 Checklist for New Features
|
## 📋 Checklist for New Features
|
||||||
|
|
||||||
### Before Implementation
|
### Before Implementation
|
||||||
- [ ] Define interfaces in domain layer
|
- [ ] Define interfaces in the same package (`repository.go`)
|
||||||
- [ ] Create proper request/response forms with govalidator tags
|
- [ ] Create proper request/response forms with govalidator tags (`form.go`)
|
||||||
- [ ] Plan error handling strategy
|
- [ ] Plan error handling strategy
|
||||||
- [ ] Design database schema if needed
|
- [ ] Design database schema if needed
|
||||||
- [ ] Consider caching requirements
|
- [ ] Consider caching requirements
|
||||||
- [ ] Define custom validators if needed
|
- [ ] Define custom validators if needed
|
||||||
|
|
||||||
### During Implementation
|
### During Implementation
|
||||||
- [ ] Follow clean architecture layers
|
- [ ] Follow flat domain structure (all files in same package)
|
||||||
- [ ] Use dependency injection
|
- [ ] Use dependency injection
|
||||||
- [ ] Implement proper logging
|
- [ ] Implement proper logging
|
||||||
- [ ] Add govalidator validation tags to forms
|
- [ ] Add govalidator validation tags to forms
|
||||||
@@ -342,498 +335,16 @@ log.Info("Customer login")
|
|||||||
- [ ] Check performance impact
|
- [ ] Check performance impact
|
||||||
- [ ] Test validation scenarios
|
- [ ] 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
|
## 🚀 Remember
|
||||||
- Prioritize simplicity and readability over cleverness
|
- Prioritize simplicity and readability over cleverness
|
||||||
- Make code self-documenting through clear naming
|
- Make code self-documenting through clear naming
|
||||||
- Always consider the person who will maintain this code
|
- Always consider the person who will maintain this code
|
||||||
- Test your code thoroughly
|
- Test your code thoroughly
|
||||||
- Security and performance are not optional
|
- 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
|
- Use the custom logger interface consistently
|
||||||
- Implement proper error handling and logging
|
- 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 Consistency**: Always use Unix timestamps (int64) for input, storage, and output
|
||||||
- **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges
|
- **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges
|
||||||
- **Time Utilities**: Use centralized time utility functions for consistency across the application
|
- **Time Utilities**: Use centralized time utility functions for consistency across the application
|
||||||
@@ -841,3 +352,4 @@ func main() {
|
|||||||
- **Validation**: Always use govalidator for request validation with proper tags
|
- **Validation**: Always use govalidator for request validation with proper tags
|
||||||
- **Form Design**: Define validation rules in request forms, not in handlers
|
- **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
|
||||||
+22
-508
@@ -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)
|
- **Infrastructure Layer** (`infra/`): External dependencies (DB, cache, queues, config)
|
||||||
|
|
||||||
### Domain Structure Pattern
|
### Domain Structure Pattern
|
||||||
Each domain follows this structure:
|
Each domain follows this flat structure:
|
||||||
```
|
```
|
||||||
internal/{domain}/
|
internal/{domain}/
|
||||||
├── domain/
|
├── entity.go # Core domain entities
|
||||||
│ ├── entity/
|
├── aggregate.go # Aggregate roots and DTOs
|
||||||
│ │ └── entity.go # Core domain entities
|
├── repository.go # Repository interface and implementation
|
||||||
│ ├── aggregate/
|
├── service.go # Business logic and use cases
|
||||||
│ │ └── aggregate.go # Aggregate roots and DTOs
|
├── form.go # Request/response forms with validation
|
||||||
│ ├── {domain}.go # Repository implementation
|
└── handler.go # HTTP controllers and request/response handling
|
||||||
│ └── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Package Structure**: All files in a domain use the same package name (e.g., `package customer`)
|
||||||
|
|
||||||
### Dependency Rules
|
### Dependency Rules
|
||||||
- Always depend on interfaces, not concrete implementations
|
- Always depend on interfaces, not concrete implementations
|
||||||
- Use dependency injection through constructors
|
- Use dependency injection through constructors
|
||||||
- Inner layers should never depend on outer layers
|
- All files in a domain use the same package name for easy access
|
||||||
- All dependencies flow inward toward the domain
|
- Repository implementations are in the same package as interfaces
|
||||||
- Repository implementations are embedded in domain package
|
- Keep dependencies minimal and focused
|
||||||
|
|
||||||
## 💻 Go Best Practices
|
## 💻 Go Best Practices
|
||||||
|
|
||||||
@@ -72,17 +65,19 @@ internal/{domain}/
|
|||||||
## 🔌 Interface Design
|
## 🔌 Interface Design
|
||||||
|
|
||||||
### Repository Interfaces
|
### 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
|
- Use context.Context as first parameter for all methods
|
||||||
- Return domain entities, not database models
|
- Return domain entities, not database models
|
||||||
- Include common operations: Create, GetByID, Update, Delete, List
|
- Include common operations: Create, GetByID, Update, Delete, List
|
||||||
- Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`)
|
- Use specific query methods (e.g., `GetByEmail`, `GetByCompanyID`)
|
||||||
|
- Repository implementation is in the same file as the interface
|
||||||
|
|
||||||
### Service Interfaces
|
### Service Interfaces
|
||||||
- Define business operations clearly in service package
|
- Define business operations clearly in service.go
|
||||||
- Use request/response forms for complex operations
|
- Use request/response forms for complex operations
|
||||||
- Include validation in service methods
|
- Include validation in service methods
|
||||||
- Handle business logic, not infrastructure concerns
|
- Handle business logic, not infrastructure concerns
|
||||||
|
- Service implementation is in the same package as other domain files
|
||||||
|
|
||||||
## 📝 Logging Standards
|
## 📝 Logging Standards
|
||||||
|
|
||||||
@@ -314,15 +309,15 @@ log.Info("Customer login")
|
|||||||
## 📋 Checklist for New Features
|
## 📋 Checklist for New Features
|
||||||
|
|
||||||
### Before Implementation
|
### Before Implementation
|
||||||
- [ ] Define interfaces in domain layer
|
- [ ] Define interfaces in the same package (`repository.go`)
|
||||||
- [ ] Create proper request/response forms with govalidator tags
|
- [ ] Create proper request/response forms with govalidator tags (`form.go`)
|
||||||
- [ ] Plan error handling strategy
|
- [ ] Plan error handling strategy
|
||||||
- [ ] Design database schema if needed
|
- [ ] Design database schema if needed
|
||||||
- [ ] Consider caching requirements
|
- [ ] Consider caching requirements
|
||||||
- [ ] Define custom validators if needed
|
- [ ] Define custom validators if needed
|
||||||
|
|
||||||
### During Implementation
|
### During Implementation
|
||||||
- [ ] Follow clean architecture layers
|
- [ ] Follow flat domain structure (all files in same package)
|
||||||
- [ ] Use dependency injection
|
- [ ] Use dependency injection
|
||||||
- [ ] Implement proper logging
|
- [ ] Implement proper logging
|
||||||
- [ ] Add govalidator validation tags to forms
|
- [ ] Add govalidator validation tags to forms
|
||||||
@@ -337,498 +332,16 @@ log.Info("Customer login")
|
|||||||
- [ ] Check performance impact
|
- [ ] Check performance impact
|
||||||
- [ ] Test validation scenarios
|
- [ ] 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
|
## 🚀 Remember
|
||||||
- Prioritize simplicity and readability over cleverness
|
- Prioritize simplicity and readability over cleverness
|
||||||
- Make code self-documenting through clear naming
|
- Make code self-documenting through clear naming
|
||||||
- Always consider the person who will maintain this code
|
- Always consider the person who will maintain this code
|
||||||
- Test your code thoroughly
|
- Test your code thoroughly
|
||||||
- Security and performance are not optional
|
- 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
|
- Use the custom logger interface consistently
|
||||||
- Implement proper error handling and logging
|
- 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 Consistency**: Always use Unix timestamps (int64) for input, storage, and output
|
||||||
- **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges
|
- **Time Validation**: Validate all Unix timestamp inputs are within reasonable ranges
|
||||||
- **Time Utilities**: Use centralized time utility functions for consistency across the application
|
- **Time Utilities**: Use centralized time utility functions for consistency across the application
|
||||||
@@ -836,3 +349,4 @@ func main() {
|
|||||||
- **Validation**: Always use govalidator for request validation with proper tags
|
- **Validation**: Always use govalidator for request validation with proper tags
|
||||||
- **Form Design**: Define validation rules in request forms, not in handlers
|
- **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
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)"`
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package v1
|
package customer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
service "tm/internal/customer/service"
|
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
|
|
||||||
@@ -11,13 +10,13 @@ import (
|
|||||||
|
|
||||||
// CustomerHandler handles authentication related HTTP requests for mobile customers
|
// CustomerHandler handles authentication related HTTP requests for mobile customers
|
||||||
type CustomerHandler struct {
|
type CustomerHandler struct {
|
||||||
service service.CustomerService
|
service CustomerService
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCustomerHandler creates a new customer authentication handler
|
// NewCustomerHandler creates a new customer authentication handler
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
customerAuthService service.CustomerService,
|
customerAuthService CustomerService,
|
||||||
logger logger.Logger,
|
logger logger.Logger,
|
||||||
) *CustomerHandler {
|
) *CustomerHandler {
|
||||||
return &CustomerHandler{
|
return &CustomerHandler{
|
||||||
@@ -39,7 +38,7 @@ func NewHandler(
|
|||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/mobile/auth/register [post]
|
// @Router /api/mobile/auth/register [post]
|
||||||
func (h *CustomerHandler) Register(c echo.Context) error {
|
func (h *CustomerHandler) Register(c echo.Context) error {
|
||||||
var req service.RegisterForm
|
var req RegisterForm
|
||||||
|
|
||||||
// Bind request body
|
// Bind request body
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
@@ -73,7 +72,7 @@ func (h *CustomerHandler) Register(c echo.Context) error {
|
|||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/mobile/auth/login [post]
|
// @Router /api/mobile/auth/login [post]
|
||||||
func (h *CustomerHandler) Login(c echo.Context) error {
|
func (h *CustomerHandler) Login(c echo.Context) error {
|
||||||
var req service.LoginForm
|
var req LoginForm
|
||||||
|
|
||||||
// Bind request body
|
// Bind request body
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
@@ -107,7 +106,7 @@ func (h *CustomerHandler) Login(c echo.Context) error {
|
|||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/mobile/auth/refresh [post]
|
// @Router /api/mobile/auth/refresh [post]
|
||||||
func (h *CustomerHandler) RefreshToken(c echo.Context) error {
|
func (h *CustomerHandler) RefreshToken(c echo.Context) error {
|
||||||
var req service.RefreshTokenForm
|
var req RefreshTokenForm
|
||||||
|
|
||||||
// Bind request body
|
// Bind request body
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
@@ -142,7 +141,7 @@ func (h *CustomerHandler) RefreshToken(c echo.Context) error {
|
|||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/mobile/auth/change-password [post]
|
// @Router /api/mobile/auth/change-password [post]
|
||||||
func (h *CustomerHandler) ChangePassword(c echo.Context) error {
|
func (h *CustomerHandler) ChangePassword(c echo.Context) error {
|
||||||
var req service.ChangePasswordForm
|
var req ChangePasswordForm
|
||||||
|
|
||||||
// Bind request body
|
// Bind request body
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
"tm/internal/customer/domain/entity"
|
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -13,6 +12,19 @@ import (
|
|||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"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
|
// customerRepository implements the Repository interface
|
||||||
type customerRepository struct {
|
type customerRepository struct {
|
||||||
collection *mongo.Collection
|
collection *mongo.Collection
|
||||||
@@ -68,9 +80,9 @@ func NewCustomerRepository(db *mongo.Database, logger logger.Logger) Repository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new customer
|
// Create creates a new customer
|
||||||
func (r *customerRepository) Create(ctx context.Context, customer *entity.Customer) error {
|
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
|
||||||
// Set created/updated timestamps
|
// Set created/updated timestamps using Unix timestamps
|
||||||
now := time.Now()
|
now := time.Now().Unix()
|
||||||
customer.CreatedAt = now
|
customer.CreatedAt = now
|
||||||
customer.UpdatedAt = now
|
customer.UpdatedAt = now
|
||||||
|
|
||||||
@@ -97,8 +109,8 @@ func (r *customerRepository) Create(ctx context.Context, customer *entity.Custom
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByID retrieves a customer by ID
|
// GetByID retrieves a customer by ID
|
||||||
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entity.Customer, error) {
|
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
|
||||||
var customer entity.Customer
|
var customer Customer
|
||||||
|
|
||||||
filter := bson.M{"_id": id}
|
filter := bson.M{"_id": id}
|
||||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
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
|
// GetByEmail retrieves a customer by email
|
||||||
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*entity.Customer, error) {
|
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) {
|
||||||
var customer entity.Customer
|
var customer Customer
|
||||||
|
|
||||||
filter := bson.M{"email": email}
|
filter := bson.M{"email": email}
|
||||||
err := r.collection.FindOne(ctx, filter).Decode(&customer)
|
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
|
// Update updates a customer
|
||||||
func (r *customerRepository) Update(ctx context.Context, customer *entity.Customer) error {
|
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
|
||||||
// Set updated timestamp
|
// Set updated timestamp using Unix timestamp
|
||||||
customer.UpdatedAt = time.Now()
|
customer.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
filter := bson.M{"_id": customer.ID}
|
filter := bson.M{"_id": customer.ID}
|
||||||
update := bson.M{"$set": customer}
|
update := bson.M{"$set": customer}
|
||||||
@@ -173,7 +185,7 @@ func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$set": bson.M{
|
"$set": bson.M{
|
||||||
"is_active": false,
|
"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
|
// List retrieves customers with pagination
|
||||||
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*entity.Customer, error) {
|
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
|
||||||
var customers []*entity.Customer
|
var customers []*Customer
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
opts := options.Find()
|
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
|
// GetByCompanyID retrieves customers by company ID with pagination
|
||||||
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*entity.Customer, error) {
|
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
|
||||||
var customers []*entity.Customer
|
var customers []*Customer
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
opts := options.Find()
|
opts := options.Find()
|
||||||
@@ -275,7 +287,7 @@ func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, t
|
|||||||
filter := bson.M{"_id": id}
|
filter := bson.M{"_id": id}
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates
|
"$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)
|
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}
|
filter := bson.M{"_id": id}
|
||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token
|
"$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)
|
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||||
@@ -4,17 +4,155 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
infrastructure "tm/infra"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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
|
// 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{}{
|
s.logger.Info("Registering new customer", map[string]interface{}{
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
})
|
})
|
||||||
@@ -35,7 +173,7 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create customer
|
// Create customer
|
||||||
customer := &entity.Customer{
|
customer := &Customer{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
@@ -45,8 +183,8 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*aggr
|
|||||||
IsActive: true,
|
IsActive: true,
|
||||||
IsVerified: false, // Require email verification
|
IsVerified: false, // Require email verification
|
||||||
DeviceTokens: make([]string, 0),
|
DeviceTokens: make([]string, 0),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now().Unix(),
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.customerRepo.Create(ctx, customer); err != nil {
|
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
|
// TODO: Send verification email
|
||||||
s.sendVerificationEmail(ctx, customer)
|
s.sendVerificationEmail(ctx, customer)
|
||||||
|
|
||||||
return &aggregate.AuthorizationResponse{
|
return &AuthorizationResponse{
|
||||||
Customer: aggregate.NewCustomer(*customer),
|
Customer: NewCustomerAggregate(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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{}{
|
s.logger.Info("Customer login attempt", map[string]interface{}{
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
})
|
})
|
||||||
@@ -127,9 +265,9 @@ func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*aggregate.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update last login time
|
// Update last login time
|
||||||
customer.LastLoginAt = &time.Time{}
|
now := time.Now().Unix()
|
||||||
*customer.LastLoginAt = time.Now()
|
customer.LastLoginAt = &now
|
||||||
customer.UpdatedAt = time.Now()
|
customer.UpdatedAt = now
|
||||||
|
|
||||||
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
||||||
s.logger.Warn("Failed to update customer last login", map[string]interface{}{
|
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,
|
"is_verified": customer.IsVerified,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &aggregate.AuthorizationResponse{
|
return &AuthorizationResponse{
|
||||||
Customer: aggregate.NewCustomer(*customer),
|
Customer: NewCustomerAggregate(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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
|
// Parse and validate refresh token
|
||||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
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 nil, errors.New("failed to generate refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &aggregate.AuthorizationResponse{
|
return &AuthorizationResponse{
|
||||||
Customer: aggregate.NewCustomer(*customer),
|
Customer: NewCustomerAggregate(*customer),
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: newRefreshToken,
|
RefreshToken: newRefreshToken,
|
||||||
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
|
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
|
// 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) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, errors.New("invalid signing method")
|
return nil, errors.New("invalid signing method")
|
||||||
@@ -314,7 +452,7 @@ func (s *CustomerService) ChangePassword(ctx context.Context, customerID uuid.UU
|
|||||||
|
|
||||||
// Update password
|
// Update password
|
||||||
customer.Password = hashedPassword
|
customer.Password = hashedPassword
|
||||||
customer.UpdatedAt = time.Now()
|
customer.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
if err := s.customerRepo.Update(ctx, customer); err != nil {
|
||||||
s.logger.Error("Failed to update customer password", map[string]interface{}{
|
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
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string, error) {
|
func (s *CustomerService) generateAccessToken(customer *Customer) (string, error) {
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"customer_id": customer.ID.String(),
|
"customer_id": customer.ID.String(),
|
||||||
"email": customer.Email,
|
"email": customer.Email,
|
||||||
@@ -398,7 +536,7 @@ func (s *CustomerService) generateAccessToken(customer *entity.Customer) (string
|
|||||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
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{
|
claims := jwt.MapClaims{
|
||||||
"customer_id": customer.ID.String(),
|
"customer_id": customer.ID.String(),
|
||||||
"user_type": "customer",
|
"user_type": "customer",
|
||||||
@@ -411,7 +549,7 @@ func (s *CustomerService) generateRefreshToken(customer *entity.Customer) (strin
|
|||||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
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
|
// TODO: Implement email sending logic
|
||||||
_ = ctx
|
_ = ctx
|
||||||
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
s.logger.Info("Verification email would be sent", map[string]interface{}{
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user