diff --git a/.gitignore b/.gitignore index 13fdb4c..86c78b9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ go.work.sum *.sqlite3 bin/ -commercify \ No newline at end of file +commercify + +web/types/ \ No newline at end of file diff --git a/Makefile b/Makefile index 491e5a7..c18128c 100644 --- a/Makefile +++ b/Makefile @@ -89,11 +89,7 @@ docker-dev-push: ## Build Docker image for development docker build -t ghcr.io/zenfulcode/commercifygo:v2-dev . docker push ghcr.io/zenfulcode/commercifygo:v2-dev -# Development commands -test: ## Run tests - go test ./... - -test-verbose: ## Run tests with verbose output +test: ## Run tests with verbose output go test -v ./... clean: ## Clean build artifacts @@ -123,8 +119,8 @@ fmt: ## Format Go code vet: ## Run go vet go vet ./... -mod-tidy: ## Tidy Go modules - go mod tidy +tygo: + @tygo generate # Maintenance commands expire-checkouts: ## Expire old checkouts manually @@ -132,3 +128,5 @@ expire-checkouts: ## Expire old checkouts manually force-delete-checkouts: ## Force delete all expired, abandoned, and old completed checkouts go run ./cmd/expire-checkouts -force + + diff --git a/docs/dashboard_api_examples.md b/docs/dashboard_api_examples.md new file mode 100644 index 0000000..a10d4f6 --- /dev/null +++ b/docs/dashboard_api_examples.md @@ -0,0 +1,156 @@ +# Dashboard API Endpoints + +## GET /api/admin/dashboard/stats + +Retrieve dashboard statistics for a specified time period. + +### Authentication + +This endpoint requires admin authentication. + +### Query Parameters + +| Parameter | Type | Required | Description | +| ------------ | ------ | -------- | ------------------------------------------------------------ | +| `start_date` | string | No | Start date in YYYY-MM-DD format | +| `end_date` | string | No | End date in YYYY-MM-DD format | +| `days` | int | No | Number of days from current date (alternative to date range) | + +If no parameters are provided, defaults to the last 30 days. + +### Example Requests + +#### Get stats for the last 30 days (default) + +```http +GET /api/admin/dashboard/stats +``` + +#### Get stats for a specific number of days + +```http +GET /api/admin/dashboard/stats?days=7 +``` + +#### Get stats for a specific date range + +```http +GET /api/admin/dashboard/stats?start_date=2025-01-01&end_date=2025-01-31 +``` + +### Response + +```json +{ + "success": true, + "message": "Dashboard statistics retrieved successfully", + "data": { + "total_revenue": 4567890, + "total_orders": 234, + "total_customers": 1247, + "new_customers": 23, + "total_products": 156, + "low_stock_products": 8, + "revenue_change": { + "value": 15.5, + "direction": "up" + }, + "orders_change": { + "value": 8.2, + "direction": "up" + }, + "recent_orders": [ + { + "id": 1001, + "order_number": "ORD-2025-001", + "customer_name": "John Doe", + "customer_email": "john@example.com", + "total_amount": 12345, + "status": "completed", + "created_at": "2025-08-20T10:30:00Z" + } + ], + "top_products": [ + { + "product_id": 1, + "product_name": "Wireless Headphones", + "variant_id": 1, + "variant_name": "Black", + "quantity_sold": 45, + "revenue": 225000 + } + ], + "period_start": "2025-07-22T00:00:00Z", + "period_end": "2025-08-21T23:59:59Z" + } +} +``` + +### Response Fields + +- `total_revenue`: Total revenue in cents for the specified period +- `total_orders`: Total number of orders placed in the period +- `total_customers`: Total number of registered customers (all time) +- `new_customers`: Number of new customers registered in the period +- `total_products`: Total number of active products in the system +- `low_stock_products`: Number of products with stock at or below the low stock threshold (10 units) +- `revenue_change`: Percentage change in revenue compared to the previous equivalent period + - `value`: Absolute percentage change (e.g., 15.5 for 15.5% change) + - `direction`: "up", "down", or "stable" +- `orders_change`: Percentage change in orders compared to the previous equivalent period + - `value`: Absolute percentage change + - `direction`: "up", "down", or "stable" +- `recent_orders`: Array of recent orders (limited to 10) with basic information +- `top_products`: Array of top-selling products (limited to 10) with sales data +- `period_start`: Start of the queried period +- `period_end`: End of the queried period + +### Error Responses + +#### 400 Bad Request + +```json +{ + "success": false, + "error": "Invalid start_date format. Use YYYY-MM-DD" +} +``` + +#### 401 Unauthorized + +```json +{ + "success": false, + "error": "Authentication required" +} +``` + +#### 403 Forbidden + +```json +{ + "success": false, + "error": "Admin access required" +} +``` + +#### 500 Internal Server Error + +```json +{ + "success": false, + "error": "Failed to retrieve dashboard statistics" +} +``` + +### Notes + +- All monetary values are returned in cents (e.g., $123.45 = 12345) +- Revenue calculations include only paid, shipped, or completed orders +- Percentage changes compare the current period with the previous equivalent period + - For a 30-day period, it compares with the previous 30 days + - If there's no previous period data, changes show as 100% "up" if current period has data, or 0% "stable" if both periods have no data +- Low stock threshold is set to 10 units or less +- Top products are ranked by quantity sold +- Recent orders are ordered by creation date (most recent first) +- Guest orders are included with customer_name as "Guest" diff --git a/internal/application/usecase/dashboard_usecase.go b/internal/application/usecase/dashboard_usecase.go new file mode 100644 index 0000000..3aa376e --- /dev/null +++ b/internal/application/usecase/dashboard_usecase.go @@ -0,0 +1,174 @@ +package usecase + +import ( + "errors" + "math" + "time" + + "github.com/zenfulcode/commercify/internal/domain/dto" + "github.com/zenfulcode/commercify/internal/domain/repository" +) + +// DashboardUseCase handles dashboard-related business logic +type DashboardUseCase struct { + orderRepo repository.OrderRepository + userRepo repository.UserRepository + productRepo repository.ProductRepository +} + +// NewDashboardUseCase creates a new DashboardUseCase +func NewDashboardUseCase(orderRepo repository.OrderRepository, userRepo repository.UserRepository, productRepo repository.ProductRepository) *DashboardUseCase { + return &DashboardUseCase{ + orderRepo: orderRepo, + userRepo: userRepo, + productRepo: productRepo, + } +} + +// GetDashboardStats retrieves dashboard statistics for a given time period +func (d *DashboardUseCase) GetDashboardStats(request dto.DashboardStatsRequest) (*dto.DashboardStats, error) { + // Calculate time range + endDate := time.Now() + var startDate time.Time + + if request.StartDate != nil && request.EndDate != nil { + startDate = *request.StartDate + endDate = *request.EndDate + } else if request.Days > 0 { + startDate = endDate.AddDate(0, 0, -request.Days) + } else { + // Default to 30 days if no range specified + startDate = endDate.AddDate(0, 0, -30) + } + + // Validate date range + if startDate.After(endDate) { + return nil, errors.New("start date cannot be after end date") + } + + // Get total revenue + totalRevenue, err := d.orderRepo.GetTotalRevenueByDateRange(startDate, endDate) + if err != nil { + return nil, err + } + + // Get total orders + totalOrders, err := d.orderRepo.GetTotalOrdersByDateRange(startDate, endDate) + if err != nil { + return nil, err + } + + // Get total customers + totalCustomers, err := d.userRepo.GetTotalCustomersCount() + if err != nil { + return nil, err + } + + // Get new customers + newCustomers, err := d.userRepo.GetNewCustomersCount(startDate, endDate) + if err != nil { + return nil, err + } + + // Get recent orders (limit to 10 for dashboard) + recentOrders, err := d.orderRepo.GetRecentOrdersSummary(startDate, endDate, 10) + if err != nil { + return nil, err + } + + // Get top products (limit to 10 for dashboard) + topProducts, err := d.orderRepo.GetTopProductsSummary(startDate, endDate, 10) + if err != nil { + return nil, err + } + + // Get total products count + totalProducts, err := d.productRepo.GetTotalProductsCount() + if err != nil { + return nil, err + } + + // Get low stock products count (threshold of 10 or less) + lowStockProducts, err := d.productRepo.GetLowStockProductsCount(10) + if err != nil { + return nil, err + } + + // Calculate previous period for comparison + periodDuration := endDate.Sub(startDate) + previousStartDate := startDate.Add(-periodDuration) + previousEndDate := startDate + + // Get previous period revenue for comparison + revenueChange, err := d.calculatePercentageChange( + func() (int64, error) { + return d.orderRepo.GetTotalRevenueByDateRange(previousStartDate, previousEndDate) + }, + func() (int64, error) { return d.orderRepo.GetTotalRevenueByDateRange(startDate, endDate) }, + ) + if err != nil { + return nil, err + } + + // Get previous period orders for comparison + ordersChange, err := d.calculatePercentageChange( + func() (int64, error) { + return d.orderRepo.GetTotalOrdersByDateRange(previousStartDate, previousEndDate) + }, + func() (int64, error) { return d.orderRepo.GetTotalOrdersByDateRange(startDate, endDate) }, + ) + if err != nil { + return nil, err + } + + return &dto.DashboardStats{ + TotalRevenue: totalRevenue, + TotalOrders: totalOrders, + TotalCustomers: totalCustomers, + NewCustomers: newCustomers, + TotalProducts: totalProducts, + LowStockProducts: lowStockProducts, + RevenueChange: revenueChange, + OrdersChange: ordersChange, + RecentOrders: recentOrders, + TopProducts: topProducts, + PeriodStart: startDate, + PeriodEnd: endDate, + }, nil +} + +// calculatePercentageChange calculates the percentage change between two values +func (d *DashboardUseCase) calculatePercentageChange(getPreviousValue, getCurrentValue func() (int64, error)) (*dto.PercentageChange, error) { + previousValue, err := getPreviousValue() + if err != nil { + return nil, err + } + + currentValue, err := getCurrentValue() + if err != nil { + return nil, err + } + + if previousValue == 0 { + // If previous value is 0, we can't calculate a percentage + if currentValue > 0 { + return &dto.PercentageChange{Value: 100.0, Direction: "up"}, nil + } + return &dto.PercentageChange{Value: 0.0, Direction: "stable"}, nil + } + + // Calculate percentage change + change := float64(currentValue-previousValue) / float64(previousValue) * 100 + + direction := "stable" + if change > 0 { + direction = "up" + } else if change < 0 { + direction = "down" + } + + return &dto.PercentageChange{ + Value: math.Abs(change), + Direction: direction, + }, nil +} diff --git a/internal/application/usecase/dashboard_usecase_test.go b/internal/application/usecase/dashboard_usecase_test.go new file mode 100644 index 0000000..cc6ddec --- /dev/null +++ b/internal/application/usecase/dashboard_usecase_test.go @@ -0,0 +1,435 @@ +package usecase + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zenfulcode/commercify/internal/domain/dto" + "github.com/zenfulcode/commercify/internal/domain/entity" + "github.com/zenfulcode/commercify/internal/infrastructure/repository/gorm" + "github.com/zenfulcode/commercify/testutil" +) + +// Integration Tests with Real Database +func TestDashboardUseCase_GetDashboardStats_WithRealData(t *testing.T) { + // Setup test database + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Create test data + now := time.Now() + + // Create test users + user1, err := entity.NewUser("test1@example.com", "password123", "John", "Doe", entity.RoleUser) + require.NoError(t, err) + err = userRepo.Create(user1) + require.NoError(t, err) + + user2, err := entity.NewUser("test2@example.com", "password123", "Jane", "Smith", entity.RoleUser) + require.NoError(t, err) + err = userRepo.Create(user2) + require.NoError(t, err) + + // Create test products and categories for orders + category := &entity.Category{ + Name: "Test Category", + Description: "Test category for integration test", + } + err = db.Create(category).Error + require.NoError(t, err) + + // Create product variant + variant, err := entity.NewProductVariant( + "TEST-SKU-001", + 10, + 9999, // 99.99 in cents + 1.0, + map[string]string{"size": "M"}, + []string{"test-image.jpg"}, + true, + ) + require.NoError(t, err) + + // Create product + product, err := entity.NewProduct( + "Test Product", + "Test product description", + "USD", + category.ID, + []string{"product-image.jpg"}, + []*entity.ProductVariant{variant}, + true, // isActive + ) + require.NoError(t, err) + err = db.Create(product).Error + require.NoError(t, err) + + // Create test orders + order1, err := entity.NewOrder( + &user1.ID, + []entity.OrderItem{ + { + ProductID: product.ID, + ProductVariantID: product.Variants[0].ID, + Quantity: 2, + Price: 9999, + }, + }, + "USD", + nil, nil, + entity.CustomerDetails{ + Email: user1.Email, + Phone: "123-456-7890", + FullName: user1.FirstName + " " + user1.LastName, + }, + ) + require.NoError(t, err) + order1.Status = entity.OrderStatusPaid + order1.CreatedAt = now.AddDate(0, 0, -5) // 5 days ago + err = orderRepo.Create(order1) + require.NoError(t, err) + + order2, err := entity.NewOrder( + &user2.ID, + []entity.OrderItem{ + { + ProductID: product.ID, + ProductVariantID: product.Variants[0].ID, + Quantity: 1, + Price: 9999, + }, + }, + "USD", + nil, nil, + entity.CustomerDetails{ + Email: user2.Email, + Phone: "098-765-4321", + FullName: user2.FirstName + " " + user2.LastName, + }, + ) + require.NoError(t, err) + order2.Status = entity.OrderStatusCompleted + order2.CreatedAt = now.AddDate(0, 0, -10) // 10 days ago + err = orderRepo.Create(order2) + require.NoError(t, err) + + // Test dashboard stats for last 30 days + request := dto.DashboardStatsRequest{Days: 30} + + result, err := dashboardUseCase.GetDashboardStats(request) + + // Assertions + require.NoError(t, err) + require.NotNil(t, result) + + // Should have revenue from both orders (2 * 9999 + 1 * 9999 = 29997) + assert.Equal(t, int64(29997), result.TotalRevenue) + assert.Equal(t, int64(2), result.TotalOrders) + assert.Equal(t, int64(2), result.TotalCustomers) + assert.Equal(t, int64(2), result.NewCustomers) // Both users created in test + + // Should have product metrics + assert.Equal(t, int64(1), result.TotalProducts) // 1 product created + assert.Equal(t, int64(1), result.LowStockProducts) // Stock is 10, threshold is 10 + + // Should have percentage changes (since there's no previous period data, these should be special values) + require.NotNil(t, result.RevenueChange) + require.NotNil(t, result.OrdersChange) + assert.Equal(t, "up", result.RevenueChange.Direction) // No previous data, so 100% up + assert.Equal(t, "up", result.OrdersChange.Direction) + + // Should have recent orders + assert.Len(t, result.RecentOrders, 2) + assert.Contains(t, []string{result.RecentOrders[0].CustomerName, result.RecentOrders[1].CustomerName}, "John Doe") + assert.Contains(t, []string{result.RecentOrders[0].CustomerName, result.RecentOrders[1].CustomerName}, "Jane Smith") + + // Should have top products + assert.Len(t, result.TopProducts, 1) + assert.Equal(t, "Test Product", result.TopProducts[0].ProductName) + assert.Equal(t, int64(3), result.TopProducts[0].QuantitySold) // 2 + 1 + assert.Equal(t, int64(29997), result.TopProducts[0].Revenue) + + // Verify the date range is approximately correct (30 days ago to now) + expectedStart := time.Now().AddDate(0, 0, -30) + assert.WithinDuration(t, expectedStart, result.PeriodStart, time.Hour*24) // Allow 1 day tolerance + assert.WithinDuration(t, time.Now(), result.PeriodEnd, time.Hour*24) +} + +func TestDashboardUseCase_GetDashboardStats_WithDateRange(t *testing.T) { + // Setup test database + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Create test data with specific dates + specificDate := time.Date(2025, 1, 15, 12, 0, 0, 0, time.UTC) + + // Create test user + user, err := entity.NewUser("test@example.com", "password123", "Test", "User", entity.RoleUser) + require.NoError(t, err) + user.CreatedAt = specificDate // Set specific creation date + err = userRepo.Create(user) + require.NoError(t, err) + + // Create category and product + category := &entity.Category{ + Name: "Test Category", + Description: "Test category", + } + err = db.Create(category).Error + require.NoError(t, err) + + variant, err := entity.NewProductVariant( + "TEST-SKU-002", + 5, + 5000, // 50.00 in cents + 0.5, + map[string]string{"color": "blue"}, + []string{}, + true, + ) + require.NoError(t, err) + + product, err := entity.NewProduct( + "Test Product 2", + "Another test product", + "USD", + category.ID, + []string{}, + []*entity.ProductVariant{variant}, + true, + ) + require.NoError(t, err) + err = db.Create(product).Error + require.NoError(t, err) + + // Create order within the date range + order, err := entity.NewOrder( + &user.ID, + []entity.OrderItem{ + { + ProductID: product.ID, + ProductVariantID: product.Variants[0].ID, + Quantity: 1, + Price: 5000, + }, + }, + "USD", + nil, nil, + entity.CustomerDetails{ + Email: user.Email, + Phone: "555-0123", + FullName: user.FirstName + " " + user.LastName, + }, + ) + require.NoError(t, err) + order.Status = entity.OrderStatusPaid + order.CreatedAt = specificDate + err = orderRepo.Create(order) + require.NoError(t, err) + + // Test with specific date range (January 1-31, 2025) + startDate := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2025, 1, 31, 23, 59, 59, 0, time.UTC) + + request := dto.DashboardStatsRequest{ + StartDate: &startDate, + EndDate: &endDate, + } + + result, err := dashboardUseCase.GetDashboardStats(request) + + // Assertions + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, int64(5000), result.TotalRevenue) + assert.Equal(t, int64(1), result.TotalOrders) + assert.Equal(t, int64(1), result.TotalCustomers) + assert.Equal(t, int64(1), result.NewCustomers) + assert.Len(t, result.RecentOrders, 1) + assert.Len(t, result.TopProducts, 1) + assert.Equal(t, startDate, result.PeriodStart) + assert.Equal(t, endDate, result.PeriodEnd) +} + +func TestDashboardUseCase_GetDashboardStats_EmptyData(t *testing.T) { + // Setup test database with no data + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Test dashboard stats with no data + request := dto.DashboardStatsRequest{Days: 30} + + result, err := dashboardUseCase.GetDashboardStats(request) + + // Assertions + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, int64(0), result.TotalRevenue) + assert.Equal(t, int64(0), result.TotalOrders) + assert.Equal(t, int64(0), result.TotalCustomers) + assert.Equal(t, int64(0), result.NewCustomers) + assert.Len(t, result.RecentOrders, 0) + assert.Len(t, result.TopProducts, 0) + + // Verify the date range is set correctly + expectedStart := time.Now().AddDate(0, 0, -30) + assert.WithinDuration(t, expectedStart, result.PeriodStart, time.Hour*24) + assert.WithinDuration(t, time.Now(), result.PeriodEnd, time.Hour*24) +} + +func TestDashboardUseCase_GetDashboardStats_DefaultRange(t *testing.T) { + // Setup test database + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Test with empty request (should default to 30 days) + request := dto.DashboardStatsRequest{} + + result, err := dashboardUseCase.GetDashboardStats(request) + + // Assertions + require.NoError(t, err) + require.NotNil(t, result) + + // Verify the date range is approximately 30 days (default) + expectedStart := time.Now().AddDate(0, 0, -30) + assert.WithinDuration(t, expectedStart, result.PeriodStart, time.Hour*24) + assert.WithinDuration(t, time.Now(), result.PeriodEnd, time.Hour*24) +} + +func TestDashboardUseCase_GetDashboardStats_InvalidDateRange(t *testing.T) { + // Setup test database + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Test with invalid date range (start after end) + startDate := time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + + request := dto.DashboardStatsRequest{ + StartDate: &startDate, + EndDate: &endDate, + } + + // Execute + result, err := dashboardUseCase.GetDashboardStats(request) + + // Assertions + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "start date cannot be after end date") +} + +func TestDashboardUseCase_GetDashboardStats_OnlyPaidOrders(t *testing.T) { + // Setup test database + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + // Setup repositories + orderRepo := gorm.NewOrderRepository(db) + userRepo := gorm.NewUserRepository(db) + productRepo := gorm.NewProductRepository(db) + dashboardUseCase := NewDashboardUseCase(orderRepo, userRepo, productRepo) + + // Create test user + user, err := entity.NewUser("test@example.com", "password123", "Test", "User", entity.RoleUser) + require.NoError(t, err) + err = userRepo.Create(user) + require.NoError(t, err) + + // Create category and product + category := &entity.Category{ + Name: "Test Category", + Description: "Test category", + } + err = db.Create(category).Error + require.NoError(t, err) + + variant, err := entity.NewProductVariant("SKU-001", 10, 1000, 1.0, map[string]string{}, []string{}, true) + require.NoError(t, err) + + product, err := entity.NewProduct("Test Product", "Description", "USD", category.ID, []string{}, []*entity.ProductVariant{variant}, true) + require.NoError(t, err) + err = db.Create(product).Error + require.NoError(t, err) + + // Create orders with different statuses + orderPaid, err := entity.NewOrder( + &user.ID, + []entity.OrderItem{{ProductID: product.ID, ProductVariantID: product.Variants[0].ID, Quantity: 1, Price: 1000}}, + "USD", nil, nil, + entity.CustomerDetails{Email: user.Email, Phone: "555-0123", FullName: "Test User"}, + ) + require.NoError(t, err) + orderPaid.Status = entity.OrderStatusPaid + err = orderRepo.Create(orderPaid) + require.NoError(t, err) + + // Create second user to avoid order number conflicts + user2, err := entity.NewUser("test2@example.com", "password456", "Test2", "User2", entity.RoleUser) + require.NoError(t, err) + err = userRepo.Create(user2) + require.NoError(t, err) + + orderPending, err := entity.NewOrder( + &user2.ID, + []entity.OrderItem{{ProductID: product.ID, ProductVariantID: product.Variants[0].ID, Quantity: 1, Price: 1000}}, + "USD", nil, nil, + entity.CustomerDetails{Email: user2.Email, Phone: "555-0124", FullName: "Test2 User2"}, + ) + require.NoError(t, err) + orderPending.Status = entity.OrderStatusPending // This should not be included in revenue + err = orderRepo.Create(orderPending) + require.NoError(t, err) + + // Test dashboard stats + request := dto.DashboardStatsRequest{Days: 30} + result, err := dashboardUseCase.GetDashboardStats(request) + + require.NoError(t, err) + require.NotNil(t, result) + + // Only paid order should contribute to revenue + assert.Equal(t, int64(1000), result.TotalRevenue) // Only the paid order + assert.Equal(t, int64(2), result.TotalOrders) // Both orders should be counted in total orders + + // Top products should only include quantities from paid orders + assert.Len(t, result.TopProducts, 1) + assert.Equal(t, int64(1), result.TopProducts[0].QuantitySold) // Only from paid order + assert.Equal(t, int64(1000), result.TopProducts[0].Revenue) // Only from paid order +} diff --git a/internal/domain/dto/dashboard.go b/internal/domain/dto/dashboard.go new file mode 100644 index 0000000..3610610 --- /dev/null +++ b/internal/domain/dto/dashboard.go @@ -0,0 +1,55 @@ +package dto + +import ( + "time" +) + +// DashboardStatsRequest represents a request for dashboard statistics +type DashboardStatsRequest struct { + StartDate *time.Time `json:"start_date,omitempty" form:"start_date"` + EndDate *time.Time `json:"end_date,omitempty" form:"end_date"` + Days int `json:"days,omitempty" form:"days"` // Alternative to date range, defaults to 30 +} + +// PercentageChange represents a percentage change with value and direction +type PercentageChange struct { + Value float64 `json:"value"` // percentage change (e.g., 15.5 for +15.5%) + Direction string `json:"direction"` // "up", "down", or "stable" +} + +// DashboardStats represents aggregated dashboard statistics +type DashboardStats struct { + TotalRevenue int64 `json:"total_revenue"` // in cents + TotalOrders int64 `json:"total_orders"` + TotalCustomers int64 `json:"total_customers"` + NewCustomers int64 `json:"new_customers"` + TotalProducts int64 `json:"total_products"` + LowStockProducts int64 `json:"low_stock_products"` + RevenueChange *PercentageChange `json:"revenue_change"` // vs previous period + OrdersChange *PercentageChange `json:"orders_change"` // vs previous period + RecentOrders []RecentOrderSummary `json:"recent_orders"` + TopProducts []TopProductSummary `json:"top_products"` + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` +} + +// RecentOrderSummary represents a summary of recent orders for dashboard +type RecentOrderSummary struct { + ID uint `json:"id"` + OrderNumber string `json:"order_number"` + CustomerName string `json:"customer_name"` + CustomerEmail string `json:"customer_email"` + TotalAmount int64 `json:"total_amount"` // in cents + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +// TopProductSummary represents top selling products for dashboard +type TopProductSummary struct { + ProductID uint `json:"product_id"` + ProductName string `json:"product_name"` + VariantID *uint `json:"variant_id,omitempty"` + VariantName string `json:"variant_name,omitempty"` + QuantitySold int64 `json:"quantity_sold"` + Revenue int64 `json:"revenue"` // in cents +} diff --git a/internal/domain/repository/order_repository.go b/internal/domain/repository/order_repository.go index de21f89..f5a867d 100644 --- a/internal/domain/repository/order_repository.go +++ b/internal/domain/repository/order_repository.go @@ -1,6 +1,11 @@ package repository -import "github.com/zenfulcode/commercify/internal/domain/entity" +import ( + "time" + + "github.com/zenfulcode/commercify/internal/domain/dto" + "github.com/zenfulcode/commercify/internal/domain/entity" +) // OrderRepository defines the interface for order data access type OrderRepository interface { @@ -14,4 +19,10 @@ type OrderRepository interface { GetByPaymentID(paymentID string) (*entity.Order, error) ListAll(offset, limit int) ([]*entity.Order, error) HasOrdersWithProduct(productID uint) (bool, error) + + // Dashboard statistics methods + GetTotalRevenueByDateRange(startDate, endDate time.Time) (int64, error) + GetTotalOrdersByDateRange(startDate, endDate time.Time) (int64, error) + GetRecentOrdersSummary(startDate, endDate time.Time, limit int) ([]dto.RecentOrderSummary, error) + GetTopProductsSummary(startDate, endDate time.Time, limit int) ([]dto.TopProductSummary, error) } diff --git a/internal/domain/repository/product_repository.go b/internal/domain/repository/product_repository.go index 6a0b444..2fb192e 100644 --- a/internal/domain/repository/product_repository.go +++ b/internal/domain/repository/product_repository.go @@ -13,6 +13,8 @@ type ProductRepository interface { List(query, currency string, categoryID, offset, limit uint, minPriceCents, maxPriceCents int64, active bool) ([]*entity.Product, error) Count(searchQuery, currency string, categoryID uint, minPriceCents, maxPriceCents int64, active bool) (int, error) HasProductsWithCategory(categoryID uint) (bool, error) + GetTotalProductsCount() (int64, error) + GetLowStockProductsCount(lowStockThreshold int) (int64, error) } // CategoryRepository defines the interface for category data access diff --git a/internal/domain/repository/user_repository.go b/internal/domain/repository/user_repository.go index a8b6f5b..1ae3823 100644 --- a/internal/domain/repository/user_repository.go +++ b/internal/domain/repository/user_repository.go @@ -1,6 +1,10 @@ package repository -import "github.com/zenfulcode/commercify/internal/domain/entity" +import ( + "time" + + "github.com/zenfulcode/commercify/internal/domain/entity" +) // UserRepository defines the interface for user data access type UserRepository interface { @@ -10,4 +14,8 @@ type UserRepository interface { Update(user *entity.User) error Delete(id uint) error List(offset, limit int) ([]*entity.User, error) + + // Dashboard statistics methods + GetTotalCustomersCount() (int64, error) + GetNewCustomersCount(startDate, endDate time.Time) (int64, error) } diff --git a/internal/infrastructure/container/handler_provider.go b/internal/infrastructure/container/handler_provider.go index 1255720..b2c01f7 100644 --- a/internal/infrastructure/container/handler_provider.go +++ b/internal/infrastructure/container/handler_provider.go @@ -21,6 +21,7 @@ type HandlerProvider interface { CurrencyHandler() *handler.CurrencyHandler HealthHandler() *handler.HealthHandler EmailTestHandler() *handler.EmailTestHandler + DashboardHandler() *handler.DashboardHandler } // handlerProvider is the concrete implementation of HandlerProvider @@ -41,6 +42,7 @@ type handlerProvider struct { currencyHandler *handler.CurrencyHandler healthHandler *handler.HealthHandler emailTestHandler *handler.EmailTestHandler + dashboardHandler *handler.DashboardHandler } // NewHandlerProvider creates a new handler provider @@ -245,3 +247,17 @@ func (p *handlerProvider) WebhookHandlerProvider() *handler.WebhookHandlerProvid } return p.webhookHandlerProvider } + +// DashboardHandler returns the dashboard handler +func (p *handlerProvider) DashboardHandler() *handler.DashboardHandler { + p.mu.Lock() + defer p.mu.Unlock() + + if p.dashboardHandler == nil { + p.dashboardHandler = handler.NewDashboardHandler( + p.container.UseCases().DashboardUseCase(), + p.container.Logger(), + ) + } + return p.dashboardHandler +} diff --git a/internal/infrastructure/container/usecase_provider.go b/internal/infrastructure/container/usecase_provider.go index f93cef0..36e1d41 100644 --- a/internal/infrastructure/container/usecase_provider.go +++ b/internal/infrastructure/container/usecase_provider.go @@ -16,6 +16,7 @@ type UseCaseProvider interface { DiscountUseCase() *usecase.DiscountUseCase ShippingUseCase() *usecase.ShippingUseCase CurrencyUsecase() *usecase.CurrencyUseCase + DashboardUseCase() *usecase.DashboardUseCase } // useCaseProvider is the concrete implementation of UseCaseProvider @@ -23,14 +24,15 @@ type useCaseProvider struct { container Container mu sync.Mutex - userUseCase *usecase.UserUseCase - productUseCase *usecase.ProductUseCase - categoryUseCase *usecase.CategoryUseCase - checkoutUseCase *usecase.CheckoutUseCase - orderUseCase *usecase.OrderUseCase - discountUseCase *usecase.DiscountUseCase - shippingUseCase *usecase.ShippingUseCase - currencyUseCase *usecase.CurrencyUseCase + userUseCase *usecase.UserUseCase + productUseCase *usecase.ProductUseCase + categoryUseCase *usecase.CategoryUseCase + checkoutUseCase *usecase.CheckoutUseCase + orderUseCase *usecase.OrderUseCase + discountUseCase *usecase.DiscountUseCase + shippingUseCase *usecase.ShippingUseCase + currencyUseCase *usecase.CurrencyUseCase + dashboardUseCase *usecase.DashboardUseCase } // NewUseCaseProvider creates a new use case provider @@ -191,3 +193,18 @@ func (p *useCaseProvider) CurrencyUsecase() *usecase.CurrencyUseCase { } return p.currencyUseCase } + +// DashboardUseCase returns the dashboard use case +func (p *useCaseProvider) DashboardUseCase() *usecase.DashboardUseCase { + p.mu.Lock() + defer p.mu.Unlock() + + if p.dashboardUseCase == nil { + p.dashboardUseCase = usecase.NewDashboardUseCase( + p.container.Repositories().OrderRepository(), + p.container.Repositories().UserRepository(), + p.container.Repositories().ProductRepository(), + ) + } + return p.dashboardUseCase +} diff --git a/internal/infrastructure/repository/gorm/order_repository.go b/internal/infrastructure/repository/gorm/order_repository.go index cfce13b..313adb6 100644 --- a/internal/infrastructure/repository/gorm/order_repository.go +++ b/internal/infrastructure/repository/gorm/order_repository.go @@ -3,7 +3,9 @@ package gorm import ( "errors" "fmt" + "time" + "github.com/zenfulcode/commercify/internal/domain/dto" "github.com/zenfulcode/commercify/internal/domain/entity" "github.com/zenfulcode/commercify/internal/domain/repository" "gorm.io/gorm" @@ -65,7 +67,6 @@ func (o *OrderRepository) GetByPaymentID(paymentID string) (*entity.Order, error func (o *OrderRepository) GetByUser(userID uint, offset int, limit int) ([]*entity.Order, error) { var orders []*entity.Order if err := o.db.Preload("Items").Preload("Items.Product").Preload("Items.ProductVariant"). - Preload("User"). Where("user_id = ?", userID). Offset(offset).Limit(limit). Order("created_at DESC"). @@ -78,10 +79,11 @@ func (o *OrderRepository) GetByUser(userID uint, offset int, limit int) ([]*enti // HasOrdersWithProduct implements repository.OrderRepository. func (o *OrderRepository) HasOrdersWithProduct(productID uint) (bool, error) { var count int64 - if err := o.db.Model(&entity.Order{}). - Joins("JOIN order_items ON orders.id = order_items.order_id"). + err := o.db.Table("order_items"). + Joins("JOIN orders ON order_items.order_id = orders.id"). Where("order_items.product_id = ?", productID). - Count(&count).Error; err != nil { + Count(&count).Error + if err != nil { return false, fmt.Errorf("failed to check orders with product %d: %w", productID, err) } return count > 0, nil @@ -90,9 +92,10 @@ func (o *OrderRepository) HasOrdersWithProduct(productID uint) (bool, error) { // IsDiscountIdUsed implements repository.OrderRepository. func (o *OrderRepository) IsDiscountIdUsed(discountID uint) (bool, error) { var count int64 - if err := o.db.Model(&entity.Order{}). - Where("discount_discount_id = ?", discountID). - Count(&count).Error; err != nil { + err := o.db.Model(&entity.Order{}). + Where("JSON_EXTRACT(applied_discount, '$.id') = ?", discountID). + Count(&count).Error + if err != nil { return false, fmt.Errorf("failed to check if discount %d is used: %w", discountID, err) } return count > 0, nil @@ -130,6 +133,79 @@ func (o *OrderRepository) Update(order *entity.Order) error { return o.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(order).Error } +// GetTotalRevenueByDateRange implements repository.OrderRepository. +func (o *OrderRepository) GetTotalRevenueByDateRange(startDate, endDate time.Time) (int64, error) { + var totalRevenue int64 + err := o.db.Model(&entity.Order{}). + Where("created_at >= ? AND created_at <= ? AND status IN (?)", + startDate, endDate, []entity.OrderStatus{entity.OrderStatusPaid, entity.OrderStatusShipped, entity.OrderStatusCompleted}). + Select("COALESCE(SUM(total_amount), 0)"). + Scan(&totalRevenue).Error + if err != nil { + return 0, fmt.Errorf("failed to get total revenue: %w", err) + } + return totalRevenue, nil +} + +// GetTotalOrdersByDateRange implements repository.OrderRepository. +func (o *OrderRepository) GetTotalOrdersByDateRange(startDate, endDate time.Time) (int64, error) { + var count int64 + err := o.db.Model(&entity.Order{}). + Where("created_at >= ? AND created_at <= ?", startDate, endDate). + Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to get total orders count: %w", err) + } + return count, nil +} + +// GetRecentOrdersSummary implements repository.OrderRepository. +func (o *OrderRepository) GetRecentOrdersSummary(startDate, endDate time.Time, limit int) ([]dto.RecentOrderSummary, error) { + var results []dto.RecentOrderSummary + + err := o.db.Table("orders"). + Select("orders.id, orders.order_number, orders.total_amount, orders.status, orders.created_at, "+ + "CASE WHEN orders.user_id IS NULL THEN 'Guest' ELSE (users.first_name || ' ' || users.last_name) END as customer_name, "+ + "CASE WHEN orders.user_id IS NULL THEN orders.customer_email ELSE users.email END as customer_email"). + Joins("LEFT JOIN users ON orders.user_id = users.id"). + Where("orders.created_at >= ? AND orders.created_at <= ?", startDate, endDate). + Order("orders.created_at DESC"). + Limit(limit). + Scan(&results).Error + + if err != nil { + return nil, fmt.Errorf("failed to get recent orders summary: %w", err) + } + + return results, nil +} + +// GetTopProductsSummary implements repository.OrderRepository. +func (o *OrderRepository) GetTopProductsSummary(startDate, endDate time.Time, limit int) ([]dto.TopProductSummary, error) { + var results []dto.TopProductSummary + + err := o.db.Table("order_items"). + Select("order_items.product_id, products.name as product_name, "+ + "order_items.product_variant_id, COALESCE(product_variants.sku, '') as variant_name, "+ + "SUM(order_items.quantity) as quantity_sold, "+ + "SUM(order_items.price * order_items.quantity) as revenue"). + Joins("JOIN products ON order_items.product_id = products.id"). + Joins("LEFT JOIN product_variants ON order_items.product_variant_id = product_variants.id"). + Joins("JOIN orders ON order_items.order_id = orders.id"). + Where("orders.created_at >= ? AND orders.created_at <= ? AND orders.status IN (?)", + startDate, endDate, []entity.OrderStatus{entity.OrderStatusPaid, entity.OrderStatusShipped, entity.OrderStatusCompleted}). + Group("order_items.product_id, products.name, order_items.product_variant_id, product_variants.sku"). + Order("quantity_sold DESC"). + Limit(limit). + Scan(&results).Error + + if err != nil { + return nil, fmt.Errorf("failed to get top products summary: %w", err) + } + + return results, nil +} + // NewOrderRepository creates a new GORM-based OrderRepository func NewOrderRepository(db *gorm.DB) repository.OrderRepository { return &OrderRepository{db: db} diff --git a/internal/infrastructure/repository/gorm/product_repository.go b/internal/infrastructure/repository/gorm/product_repository.go index 3653e52..cf35e2a 100644 --- a/internal/infrastructure/repository/gorm/product_repository.go +++ b/internal/infrastructure/repository/gorm/product_repository.go @@ -217,3 +217,31 @@ func (r *ProductRepository) HasProductsWithCategory(categoryID uint) (bool, erro } return count > 0, nil } + +// GetTotalProductsCount returns the total number of active products +func (r *ProductRepository) GetTotalProductsCount() (int64, error) { + var count int64 + if err := r.db.Model(&entity.Product{}).Where("active = ?", true).Count(&count).Error; err != nil { + return 0, fmt.Errorf("failed to count total products: %w", err) + } + return count, nil +} + +// GetLowStockProductsCount returns the number of products with stock below the threshold +func (r *ProductRepository) GetLowStockProductsCount(lowStockThreshold int) (int64, error) { + var count int64 + + // Count products that have variants with stock below the threshold + query := ` + SELECT COUNT(DISTINCT p.id) + FROM products p + JOIN product_variants pv ON p.id = pv.product_id + WHERE p.active = true + AND pv.stock <= ?` + + if err := r.db.Raw(query, lowStockThreshold).Scan(&count).Error; err != nil { + return 0, fmt.Errorf("failed to count low stock products: %w", err) + } + + return count, nil +} diff --git a/internal/infrastructure/repository/gorm/user_repository.go b/internal/infrastructure/repository/gorm/user_repository.go index 3bea2d8..ac902b9 100644 --- a/internal/infrastructure/repository/gorm/user_repository.go +++ b/internal/infrastructure/repository/gorm/user_repository.go @@ -3,6 +3,7 @@ package gorm import ( "errors" "fmt" + "time" "github.com/zenfulcode/commercify/internal/domain/entity" "github.com/zenfulcode/commercify/internal/domain/repository" @@ -62,6 +63,28 @@ func (u *UserRepository) Update(user *entity.User) error { return u.db.Save(user).Error } +// GetTotalCustomersCount implements repository.UserRepository. +func (u *UserRepository) GetTotalCustomersCount() (int64, error) { + var count int64 + err := u.db.Model(&entity.User{}).Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to get total customers count: %w", err) + } + return count, nil +} + +// GetNewCustomersCount implements repository.UserRepository. +func (u *UserRepository) GetNewCustomersCount(startDate, endDate time.Time) (int64, error) { + var count int64 + err := u.db.Model(&entity.User{}). + Where("created_at >= ? AND created_at <= ?", startDate, endDate). + Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to get new customers count: %w", err) + } + return count, nil +} + // NewUserRepository creates a new GORM-based UserRepository func NewUserRepository(db *gorm.DB) repository.UserRepository { return &UserRepository{db: db} diff --git a/internal/interfaces/api/handler/dashboard_handler.go b/internal/interfaces/api/handler/dashboard_handler.go new file mode 100644 index 0000000..fa03d3d --- /dev/null +++ b/internal/interfaces/api/handler/dashboard_handler.go @@ -0,0 +1,97 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/zenfulcode/commercify/internal/application/usecase" + "github.com/zenfulcode/commercify/internal/domain/dto" + "github.com/zenfulcode/commercify/internal/infrastructure/logger" + "github.com/zenfulcode/commercify/internal/interfaces/api/contracts" +) + +// DashboardHandler handles dashboard-related HTTP requests +type DashboardHandler struct { + dashboardUseCase *usecase.DashboardUseCase + logger logger.Logger +} + +// NewDashboardHandler creates a new DashboardHandler +func NewDashboardHandler(dashboardUseCase *usecase.DashboardUseCase, logger logger.Logger) *DashboardHandler { + return &DashboardHandler{ + dashboardUseCase: dashboardUseCase, + logger: logger, + } +} + +// GetStats handles GET /admin/dashboard/stats +func (h *DashboardHandler) GetStats(w http.ResponseWriter, r *http.Request) { + var request dto.DashboardStatsRequest + + // Parse query parameters + query := r.URL.Query() + + // Parse start_date + if startDateStr := query.Get("start_date"); startDateStr != "" { + if startDate, err := time.Parse("2006-01-02", startDateStr); err == nil { + request.StartDate = &startDate + } else { + h.logger.Error("Invalid start_date format: %s", startDateStr) + errorResponse := contracts.ErrorResponse("Invalid start_date format. Use YYYY-MM-DD") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(errorResponse) + return + } + } + + // Parse end_date + if endDateStr := query.Get("end_date"); endDateStr != "" { + if endDate, err := time.Parse("2006-01-02", endDateStr); err == nil { + // Set to end of day + endOfDay := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, endDate.Location()) + request.EndDate = &endOfDay + } else { + h.logger.Error("Invalid end_date format: %s", endDateStr) + errorResponse := contracts.ErrorResponse("Invalid end_date format. Use YYYY-MM-DD") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(errorResponse) + return + } + } + + // Parse days + if daysStr := query.Get("days"); daysStr != "" { + if days, err := strconv.Atoi(daysStr); err == nil && days > 0 { + request.Days = days + } else { + h.logger.Error("Invalid days parameter: %s", daysStr) + errorResponse := contracts.ErrorResponse("Invalid days parameter. Must be a positive integer") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(errorResponse) + return + } + } + + // Get dashboard stats + stats, err := h.dashboardUseCase.GetDashboardStats(request) + if err != nil { + h.logger.Error("Failed to get dashboard stats: %v", err) + errorResponse := contracts.ErrorResponse("Failed to retrieve dashboard statistics") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(errorResponse) + return + } + + // Return success response + successResponse := contracts.SuccessResponseWithMessage(stats, "Dashboard statistics retrieved successfully") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(successResponse) +} diff --git a/internal/interfaces/api/server.go b/internal/interfaces/api/server.go index 154c7c3..07616cf 100644 --- a/internal/interfaces/api/server.go +++ b/internal/interfaces/api/server.go @@ -77,6 +77,7 @@ func (s *Server) setupRoutes() { currencyHandler := s.container.Handlers().CurrencyHandler() healthHandler := s.container.Handlers().HealthHandler() emailTestHandler := s.container.Handlers().EmailTestHandler() + dashboardHandler := s.container.Handlers().DashboardHandler() // Extract middleware from container authMiddleware := s.container.Middlewares().AuthMiddleware() @@ -174,6 +175,9 @@ func (s *Server) setupRoutes() { admin.HandleFunc("/currencies", currencyHandler.DeleteCurrency).Methods(http.MethodDelete) admin.HandleFunc("/currencies/default", currencyHandler.SetDefaultCurrency).Methods(http.MethodPut) + // Admin dashboard routes + admin.HandleFunc("/dashboard/stats", dashboardHandler.GetStats).Methods(http.MethodGet) + // Admin email test route admin.HandleFunc("/test/email", emailTestHandler.TestEmail).Methods(http.MethodPost) diff --git a/web/api/client.ts b/web/api/client.ts deleted file mode 100644 index a35ed0b..0000000 --- a/web/api/client.ts +++ /dev/null @@ -1,521 +0,0 @@ -/** - * @deprecated This client is deprecated. Use the new modular client from '../index.ts' instead. - * - * Migration example: - * ```typescript - * // Old way - * import { CommercifyClient } from './api/client'; - * const client = new CommercifyClient('https://api.example.com', 'token'); - * - * // New way - * import { createCommercifyClient } from '../index'; - * const client = createCommercifyClient({ - * baseUrl: 'https://api.example.com', - * token: 'token' - * }); - * - * // Usage changes: - * // client.getProducts() -> client.products.getProducts() - * // client.signIn() -> client.auth.signIn() - * // client.getGuestCheckout() -> client.checkout.getGuestCheckout() - * ``` - */ - -import { - ResponseDTO, - CreateOrderRequest, - OrderDTO, - ListResponseDTO, - ProcessPaymentRequest, - ProductDTO, - CreateProductRequest, - UpdateProductRequest, - UserDTO, - UpdateUserRequest, - UserLoginRequest, - UserLoginResponse, - CreateUserRequest, - CheckoutDTO, - UpdateCheckoutItemRequest, - SetShippingAddressRequest, - SetBillingAddressRequest, - SetCustomerDetailsRequest, - SetShippingMethodRequest, - ApplyDiscountRequest, - AddToCheckoutRequest, -} from "../types/api"; - -/** - * @deprecated Use createCommercifyClient from '../index.ts' instead - */ -export class CommercifyClient { - private baseUrl: string; - private token?: string; - - constructor(baseUrl: string, token?: string) { - this.baseUrl = baseUrl; - this.token = token; - } - - private buildUrl(endpoint: string, params?: Record): string { - const url = new URL(`${this.baseUrl}${endpoint}`); - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - if (Array.isArray(value)) { - value.forEach((v) => url.searchParams.append(key, String(v))); - } else { - url.searchParams.append(key, String(value)); - } - } - }); - } - return url.toString(); - } - - private async request( - endpoint: string, - options: RequestInit = {}, - params?: Record - ): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - ...(this.token && { Authorization: `Bearer ${this.token}` }), - ...options.headers, - }; - - const url = this.buildUrl(endpoint, params); - - try { - const response = await fetch(url, { - ...options, - headers, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - - // Create a more detailed error message - const errorMessage = - errorData?.error?.message || response.statusText || "Unknown error"; - const error = new Error(`API request failed: ${errorMessage}`); - - // Attach additional properties for error handling - (error as any).status = response.status; - (error as any).statusText = response.statusText; - (error as any).errorData = errorData; - - throw error; - } - - const data = await response.json(); - return data; - } catch (error) { - // If the error is already formatted by our code above, just rethrow it - if ((error as any).status) { - throw error; - } - - // Otherwise, it's likely a network error or other issue - console.error("API Request Error:", error); - throw new Error( - `API request failed: ${ - error instanceof Error ? error.message : "Network error" - }` - ); - } - } - - // Order endpoints - async createOrder( - orderData: CreateOrderRequest - ): Promise> { - return this.request>("/orders", { - method: "POST", - body: JSON.stringify(orderData), - }); - } - - async getOrder(orderId: string): Promise> { - return this.request>(`/orders/${orderId}`, { - method: "GET", - }); - } - - async getOrders(params?: { - page?: number; - page_size?: number; - }): Promise> { - return this.request>( - "/orders", - { - method: "GET", - }, - params - ); - } - - async getUserOrders(params?: { - page?: number; - page_size?: number; - }): Promise> { - return this.request>( - "/orders", - { - method: "GET", - }, - params - ); - } - - async processPayment( - orderId: string, - paymentData: ProcessPaymentRequest - ): Promise> { - return this.request>(`/orders/${orderId}/payment`, { - method: "POST", - body: JSON.stringify(paymentData), - }); - } - - async capturePayment(paymentId: string): Promise> { - return this.request>( - `/admin/payments/${paymentId}/capture`, - { - method: "POST", - } - ); - } - - async cancelPayment(paymentId: string): Promise> { - return this.request>( - `/admin/payments/${paymentId}/cancel`, - { - method: "POST", - } - ); - } - - async refundPayment(paymentId: string): Promise> { - return this.request>( - `/admin/payments/${paymentId}/refund`, - { - method: "POST", - } - ); - } - - async forceApproveMobilePayPayment( - paymentId: string - ): Promise> { - return this.request>( - `/admin/payments/${paymentId}/force-approve`, - { - method: "POST", - } - ); - } - - // Product endpoints - async getProducts(params?: { - page?: number; - page_size?: number; - category_id?: number; - currency?: string; - }): Promise> { - return this.request>("/products", {}, params); - } - - async getProduct( - productId: string, - currency?: string - ): Promise> { - return this.request>( - `/products/${productId}`, - { - method: "GET", - }, - currency ? { currency } : undefined - ); - } - - async searchProducts(params: { - query?: string; - category_id?: number; - min_price?: number; - max_price?: number; - page?: number; - page_size?: number; - }): Promise> { - return this.request>( - "/products/search", - { - method: "GET", - }, - params - ); - } - - async createProduct( - productData: CreateProductRequest - ): Promise> { - return this.request>("/products", { - method: "POST", - body: JSON.stringify(productData), - }); - } - - async updateProduct( - productId: string, - productData: UpdateProductRequest - ): Promise> { - return this.request>(`/products/${productId}`, { - method: "PUT", - body: JSON.stringify(productData), - }); - } - - async deleteProduct(productId: string): Promise> { - return this.request>(`/products/${productId}`, { - method: "DELETE", - }); - } - - // User endpoints - async getCurrentUser(): Promise> { - return this.request>("/users/me"); - } - - async updateUser(userData: UpdateUserRequest): Promise> { - return this.request>("/users/me", { - method: "PUT", - body: JSON.stringify(userData), - }); - } - - async signIn( - credentials: UserLoginRequest - ): Promise> { - return this.request>("/auth/signin", { - method: "POST", - body: JSON.stringify(credentials), - }); - } - async signUp( - userData: CreateUserRequest - ): Promise> { - return this.request>("/auth/signup", { - method: "POST", - body: JSON.stringify(userData), - }); - } - - async getOrCreateCheckout(): Promise> { - return this.request>("/api/checkout", { - method: "GET", - }); - } - - async addCheckoutItem( - data: AddToCheckoutRequest - ): Promise> { - return this.request>("/api/checkout/items", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async updateCheckoutItem( - productId: number, - data: UpdateCheckoutItemRequest - ): Promise> { - return this.request>( - `/api/checkout/items/${productId}`, - { - method: "PUT", - body: JSON.stringify(data), - } - ); - } - - async removeCheckoutItem( - productId: number - ): Promise> { - return this.request>( - `/api/checkout/items/${productId}`, - { - method: "DELETE", - } - ); - } - - async clearCheckout(): Promise> { - return this.request>("/api/checkout", { - method: "DELETE", - }); - } - - async setShippingAddress( - data: SetShippingAddressRequest - ): Promise> { - return this.request>( - "/api/checkout/shipping-address", - { - method: "PUT", - body: JSON.stringify(data), - } - ); - } - - async setBillingAddress( - data: SetBillingAddressRequest - ): Promise> { - return this.request>( - "/api/checkout/billing-address", - { - method: "PUT", - body: JSON.stringify(data), - } - ); - } - - async setCustomerDetails( - data: SetCustomerDetailsRequest - ): Promise> { - return this.request>( - "/api/checkout/customer-details", - { - method: "PUT", - body: JSON.stringify(data), - } - ); - } - - async setShippingMethod( - data: SetShippingMethodRequest - ): Promise> { - return this.request>( - "/api/checkout/shipping-method", - { - method: "PUT", - body: JSON.stringify(data), - } - ); - } - - async applyCheckoutDiscount( - data: ApplyDiscountRequest - ): Promise> { - return this.request>("/api/checkout/discount", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async removeCheckoutDiscount(): Promise> { - return this.request>("/api/checkout/discount", { - method: "DELETE", - }); - } - - async convertCheckoutToOrder(): Promise> { - return this.request>("/api/checkout/to-order", { - method: "POST", - }); - } - - async convertGuestCheckoutToUserCheckout(): Promise< - ResponseDTO - > { - return this.request>("/api/checkout/convert", { - method: "POST", - }); - } - - // Admin checkout endpoints - async getAdminCheckouts(params?: { - page?: number; - page_size?: number; - status?: string; - }): Promise> { - return this.request>( - "/api/admin/checkouts", - {}, - params - ); - } - - async getAdminCheckoutById( - checkoutId: number - ): Promise> { - return this.request>( - `/api/admin/checkouts/${checkoutId}`, - { - method: "GET", - } - ); - } - - async deleteAdminCheckout(checkoutId: number): Promise> { - return this.request>( - `/api/admin/checkouts/${checkoutId}`, - { - method: "DELETE", - } - ); - } - - async getCheckoutsByUser( - userId: number, - params?: { - page?: number; - page_size?: number; - status?: string; - } - ): Promise> { - return this.request>( - `/api/admin/users/${userId}/checkouts`, - {}, - params - ); - } - - async getAbandonedCheckouts(): Promise> { - return this.request>( - `/api/admin/checkouts/abandoned`, - { - method: "GET", - } - ); - } - - async getExpiredCheckouts(): Promise> { - return this.request>( - `/api/admin/checkouts/expired`, - { - method: "GET", - } - ); - } -} - -// Example usage: -// const client = new CommercifyClient('https://api.commercify.com', 'your-auth-token'); -// -// // Get products with pagination and filters -// const products = await client.getProducts({ -// page: 1, -// page_size: 20, -// category_id: 123, -// currency: 'USD' -// }); -// -// // Search products with advanced filters -// const searchResults = await client.searchProducts({ -// query: 'gaming laptop', -// category_id: 123, -// min_price: 500, -// max_price: 2000, -// page: 1, -// page_size: 20 -// }); diff --git a/web/types/contracts.ts b/web/types/contracts.ts deleted file mode 100644 index 6d687af..0000000 --- a/web/types/contracts.ts +++ /dev/null @@ -1,553 +0,0 @@ -// Code generated by tygo. DO NOT EDIT. -// Generated types for Commercify API -// Do not edit this file directly -import type { -AddressDTO, -CardDetailsDTO, -CheckoutDTO, -OrderDTO, -OrderSummaryDTO, -OrderStatus, -ShippingOptionDTO, -UserDTO, -} from "./dtos"; - -////////// -// source: category_contract.go - -/** - * CreateCategoryRequest represents the data needed to create a new category - */ -export interface CreateCategoryRequest { - name: string; - description: string; - parent_id?: number /* uint */; -} -/** - * UpdateCategoryRequest represents the data needed to update an existing category - */ -export interface UpdateCategoryRequest { - name?: string; - description?: string; - parent_id?: number /* uint */; -} - -////////// -// source: checkout_contract.go - -/** - * AddToCheckoutRequest represents the data needed to add an item to a checkout - */ -export interface AddToCheckoutRequest { - sku: string; - quantity: number /* int */; - currency?: string; // Optional currency for checkout creation/updates -} -/** - * UpdateCheckoutItemRequest represents the data needed to update a checkout item - */ -export interface UpdateCheckoutItemRequest { - quantity: number /* int */; -} -/** - * SetShippingAddressRequest represents the data needed to set a shipping address - */ -export interface SetShippingAddressRequest { - address_line1: string; - address_line2: string; - city: string; - state: string; - postal_code: string; - country: string; -} -/** - * SetBillingAddressRequest represents the data needed to set a billing address - */ -export interface SetBillingAddressRequest { - address_line1: string; - address_line2: string; - city: string; - state: string; - postal_code: string; - country: string; -} -/** - * SetCustomerDetailsRequest represents the data needed to set customer details - */ -export interface SetCustomerDetailsRequest { - email: string; - phone: string; - full_name: string; -} -/** - * SetShippingMethodRequest represents the data needed to set a shipping method - */ -export interface SetShippingMethodRequest { - shipping_method_id: number /* uint */; -} -/** - * SetCurrencyRequest represents the data needed to change checkout currency - */ -export interface SetCurrencyRequest { - currency: string; -} -/** - * ApplyDiscountRequest represents the data needed to apply a discount - */ -export interface ApplyDiscountRequest { - discount_code: string; -} -/** - * CheckoutListResponse represents a paginated list of checkouts - */ -export interface CheckoutListResponse { - ListResponseDTO: ListResponseDTO; -} -/** - * CheckoutSearchRequest represents the parameters for searching checkouts - */ -export interface CheckoutSearchRequest { - user_id?: number /* uint */; - status?: string; - PaginationDTO: PaginationDTO; -} -export interface CheckoutCompleteResponse { - order: OrderSummaryDTO; - action_required?: boolean; - redirect_url?: string; -} -/** - * CompleteCheckoutRequest represents the data needed to convert a checkout to an order - */ -export interface CompleteCheckoutRequest { - payment_provider: string; - payment_data: PaymentData; -} -export interface PaymentData { - card_details?: CardDetailsDTO; - phone_number?: string; -} - -////////// -// source: common_contract.go - -/** - * PaginationDTO represents pagination parameters - */ -export interface PaginationDTO { - page: number /* int */; - page_size: number /* int */; - total: number /* int */; -} -/** - * ResponseDTO is a generic response wrapper - */ -export interface ResponseDTO { - success: boolean; - message?: string; - data?: T; - error?: string; -} -/** - * ListResponseDTO is a generic list response wrapper - */ -export interface ListResponseDTO { - success: boolean; - message?: string; - data: T[]; - pagination: PaginationDTO; - error?: string; -} - -////////// -// source: currency_contract.go - -/** - * CreateCurrencyRequest represents a request to create a new currency - */ -export interface CreateCurrencyRequest { - code: string; - name: string; - symbol: string; - exchange_rate: number /* float64 */; - is_enabled: boolean; - is_default?: boolean; -} -/** - * UpdateCurrencyRequest represents a request to update an existing currency - */ -export interface UpdateCurrencyRequest { - name?: string; - symbol?: string; - exchange_rate?: number /* float64 */; - is_enabled?: boolean; - is_default?: boolean; -} -/** - * ConvertAmountRequest represents a request to convert an amount between currencies - */ -export interface ConvertAmountRequest { - amount: number /* float64 */; - from_currency: string; - to_currency: string; -} -/** - * SetDefaultCurrencyRequest represents a request to set a currency as default - */ -export interface SetDefaultCurrencyRequest { - code: string; -} -/** - * ConvertAmountResponse represents the response for currency conversion - */ -export interface ConvertAmountResponse { - from: ConvertedAmountDTO; - to: ConvertedAmountDTO; -} -/** - * ConvertedAmountDTO represents an amount in a specific currency - */ -export interface ConvertedAmountDTO { - currency: string; - amount: number /* float64 */; - cents: number /* int64 */; -} -/** - * DeleteCurrencyResponse represents the response after deleting a currency - */ -export interface DeleteCurrencyResponse { - status: string; - message: string; -} - -////////// -// source: discount_contract.go - -/** - * CreateDiscountRequest represents the data needed to create a new discount - */ -export interface CreateDiscountRequest { - code: string; - type: string; - method: string; - value: number /* float64 */; - min_order_value?: number /* float64 */; - max_discount_value?: number /* float64 */; - product_ids?: number /* uint */[]; - category_ids?: number /* uint */[]; - start_date?: string; - end_date?: string; - usage_limit?: number /* int */; -} -/** - * UpdateDiscountRequest represents the data needed to update a discount - */ -export interface UpdateDiscountRequest { - code?: string; - type?: string; - method?: string; - value?: number /* float64 */; - min_order_value?: number /* float64 */; - max_discount_value?: number /* float64 */; - product_ids?: number /* uint */[]; - category_ids?: number /* uint */[]; - start_date: string; - end_date: string; - usage_limit?: number /* int */; - active: boolean; -} -/** - * ValidateDiscountRequest represents the data needed to validate a discount code - */ -export interface ValidateDiscountRequest { - discount_code: string; -} -/** - * ValidateDiscountResponse represents the response for discount validation - */ -export interface ValidateDiscountResponse { - valid: boolean; - reason?: string; - discount_id?: number /* uint */; - code?: string; - type?: string; - method?: string; - value?: number /* float64 */; - min_order_value?: number /* float64 */; - max_discount_value?: number /* float64 */; -} - -////////// -// source: email_contract.go - -/** - * EmailTestRequest represents the request body for testing emails - */ -export interface EmailTestRequest { - email: string; -} - -////////// -// source: order_contract.go - -/** - * CreateOrderRequest represents the data needed to create a new order - */ -export interface CreateOrderRequest { - first_name: string; - last_name: string; - email: string; - phone_number?: string; - shipping_address: AddressDTO; - billing_address: AddressDTO; - shipping_method_id: number /* uint */; -} -/** - * CreateOrderItemRequest represents the data needed to create a new order item - */ -export interface CreateOrderItemRequest { - product_id: number /* uint */; - variant_id?: number /* uint */; - quantity: number /* int */; -} -/** - * UpdateOrderRequest represents the data needed to update an existing order - */ -export interface UpdateOrderRequest { - status?: string; - payment_status?: string; - tracking_number?: string; - estimated_delivery?: string; -} -/** - * OrderSearchRequest represents the parameters for searching orders - */ -export interface OrderSearchRequest { - user_id?: number /* uint */; - status?: OrderStatus; - payment_status?: string; - start_date?: string; - end_date?: string; - pagination: PaginationDTO; -} - -////////// -// source: payments_contracts.go - -export interface CapturePaymentRequest { - amount?: number /* float64 */; // Optional when is_full is true - is_full: boolean; // Whether to capture the full amount -} -export interface RefundPaymentRequest { - amount?: number /* float64 */; // Optional when is_full is true - is_full: boolean; // Whether to refund the full captured amount -} - -////////// -// source: products_contract.go - -/** - * CreateProductRequest represents the data needed to create a new product - */ -export interface CreateProductRequest { - name: string; - description: string; - currency: string; - category_id: number /* uint */; - images: string[]; - active: boolean; - variants: CreateVariantRequest[]; -} -/** - * AttributeKeyValue represents a key-value pair for product attributes - */ -export interface AttributeKeyValue { - name: string; - value: string; -} -/** - * CreateVariantRequest represents the data needed to create a new product variant - */ -export interface CreateVariantRequest { - sku: string; - stock: number /* int */; - attributes: AttributeKeyValue[]; - images: string[]; - is_default: boolean; - weight: number /* float64 */; - price: number /* float64 */; -} -/** - * UpdateProductRequest represents the data needed to update an existing product - */ -export interface UpdateProductRequest { - name?: string; - description?: string; - currency?: string; - category_id?: number /* uint */; - images?: string[]; - active?: boolean; - variants?: UpdateVariantRequest[]; // Optional, can be nil if no variants are updated -} -/** - * UpdateVariantRequest represents the data needed to update an existing product variant - */ -export interface UpdateVariantRequest { - sku?: string; - stock?: number /* int */; - attributes?: AttributeKeyValue[]; - images?: string[]; - is_default?: boolean; - weight?: number /* float64 */; - price?: number /* float64 */; -} - -////////// -// source: shipping_contract.go - -/** - * CreateShippingMethodRequest represents the data needed to create a new shipping method - */ -export interface CreateShippingMethodRequest { - name: string; - description: string; - estimated_delivery_days: number /* int */; -} -/** - * UpdateShippingMethodRequest represents the data needed to update a shipping method - */ -export interface UpdateShippingMethodRequest { - name?: string; - description?: string; - estimated_delivery_days?: number /* int */; - active: boolean; -} -/** - * CreateShippingZoneRequest represents the data needed to create a new shipping zone - */ -export interface CreateShippingZoneRequest { - name: string; - description: string; - countries: string[]; - states: string[]; - zip_codes: string[]; -} -/** - * UpdateShippingZoneRequest represents the data needed to update a shipping zone - */ -export interface UpdateShippingZoneRequest { - name?: string; - description?: string; - countries?: string[]; - states?: string[]; - zip_codes?: string[]; - active: boolean; -} -/** - * CreateShippingRateRequest represents the data needed to create a new shipping rate - */ -export interface CreateShippingRateRequest { - shipping_method_id: number /* uint */; - shipping_zone_id: number /* uint */; - base_rate: number /* float64 */; - min_order_value: number /* float64 */; - free_shipping_threshold?: number /* float64 */; - active: boolean; -} -/** - * CreateValueBasedRateRequest represents the data needed to create a value-based rate - */ -export interface CreateValueBasedRateRequest { - shipping_rate_id: number /* uint */; - min_order_value: number /* float64 */; - max_order_value: number /* float64 */; - rate: number /* float64 */; -} -/** - * UpdateShippingRateRequest represents the data needed to update a shipping rate - */ -export interface UpdateShippingRateRequest { - base_rate?: number /* float64 */; - min_order_value?: number /* float64 */; - free_shipping_threshold?: number /* float64 */; - active: boolean; -} -/** - * CreateWeightBasedRateRequest represents the data needed to create a weight-based rate - */ -export interface CreateWeightBasedRateRequest { - shipping_rate_id: number /* uint */; - min_weight: number /* float64 */; - max_weight: number /* float64 */; - rate: number /* float64 */; -} -/** - * CalculateShippingOptionsRequest represents the request to calculate shipping options - */ -export interface CalculateShippingOptionsRequest { - address: AddressDTO; - order_value: number /* float64 */; - order_weight: number /* float64 */; -} -/** - * CalculateShippingOptionsResponse represents the response with available shipping options - */ -export interface CalculateShippingOptionsResponse { - options: ShippingOptionDTO[]; -} -/** - * CalculateShippingCostRequest represents the request to calculate shipping cost for a specific rate - */ -export interface CalculateShippingCostRequest { - order_value: number /* float64 */; - order_weight: number /* float64 */; -} -/** - * CalculateShippingCostResponse represents the response with calculated shipping cost - */ -export interface CalculateShippingCostResponse { - cost: number /* float64 */; -} - -////////// -// source: user_contract.go - -/** - * CreateUserRequest represents the data needed to create a new user - */ -export interface CreateUserRequest { - email: string; - password: string; - first_name: string; - last_name: string; -} -/** - * UpdateUserRequest represents the data needed to update an existing user - */ -export interface UpdateUserRequest { - first_name?: string; - last_name?: string; -} -/** - * UserLoginRequest represents the data needed for user login - */ -export interface UserLoginRequest { - email: string; - password: string; -} -/** - * UserLoginResponse represents the response after successful login - */ -export interface UserLoginResponse { - user: UserDTO; - access_token: string; - refresh_token: string; - expires_in: number /* int */; -} -/** - * ChangePasswordRequest represents the data needed to change a user's password - */ -export interface ChangePasswordRequest { - current_password: string; - new_password: string; -} diff --git a/web/types/dtos.ts b/web/types/dtos.ts deleted file mode 100644 index 7857885..0000000 --- a/web/types/dtos.ts +++ /dev/null @@ -1,445 +0,0 @@ -// Code generated by tygo. DO NOT EDIT. -// Generated types for Commercify API -// Do not edit this file directly - -////////// -// source: category.go - -/** - * CategoryDTO represents a category in the system - */ -export interface CategoryDTO { - id: number /* uint */; - name: string; - description: string; - parent_id?: number /* uint */; - created_at: string; - updated_at: string; -} - -////////// -// source: checkout.go - -/** - * CheckoutDTO represents a checkout session in the system - */ -export interface CheckoutDTO { - id: number /* uint */; - user_id?: number /* uint */; - session_id?: string; - items: CheckoutItemDTO[]; - status: string; - shipping_address: AddressDTO; - billing_address: AddressDTO; - shipping_method_id: number /* uint */; - shipping_option?: ShippingOptionDTO; - payment_provider?: string; - total_amount: number /* float64 */; - shipping_cost: number /* float64 */; - total_weight: number /* float64 */; - customer_details: CustomerDetailsDTO; - currency: string; - discount_code?: string; - discount_amount: number /* float64 */; - final_amount: number /* float64 */; - applied_discount?: AppliedDiscountDTO; - last_activity_at: string; - expires_at: string; -} -/** - * CheckoutItemDTO represents an item in a checkout - */ -export interface CheckoutItemDTO { - id: number /* uint */; - product_id: number /* uint */; - variant_id: number /* uint */; - product_name: string; - variant_name?: string; - image_url?: string; - sku: string; - price: number /* float64 */; - quantity: number /* int */; - weight: number /* float64 */; - subtotal: number /* float64 */; - created_at: string; - updated_at: string; -} -/** - * CardDetailsDTO represents card details for payment processing - */ -export interface CardDetailsDTO { - card_number: string; - expiry_month: number /* int */; - expiry_year: number /* int */; - cvv: string; - cardholder_name: string; - token?: string; // Optional token for saved cards -} - -////////// -// source: common.go - -/** - * AddressDTO represents a shipping or billing address - */ -export interface AddressDTO { - address_line1: string; - address_line2: string; - city: string; - state: string; - postal_code: string; - country: string; -} -/** - * CustomerDetailsDTO represents customer information for a checkout - */ -export interface CustomerDetailsDTO { - email: string; - phone: string; - full_name: string; -} -/** - * ErrorResponse represents an error response - */ -export interface ErrorResponse { - error: string; -} - -////////// -// source: currency.go - -/** - * CurrencyDTO represents a currency entity - */ -export interface CurrencyDTO { - code: string; - name: string; - symbol: string; - exchange_rate: number /* float64 */; - is_enabled: boolean; - is_default: boolean; - created_at: string; - updated_at: string; -} - -////////// -// source: discount.go - -/** - * DiscountDTO represents a discount in the system - */ -export interface DiscountDTO { - id: number /* uint */; - code: string; - type: string; - method: string; - value: number /* float64 */; - min_order_value: number /* float64 */; - max_discount_value: number /* float64 */; - product_ids?: number /* uint */[]; - category_ids?: number /* uint */[]; - start_date: string; - end_date: string; - usage_limit: number /* int */; - current_usage: number /* int */; - active: boolean; - created_at: string; - updated_at: string; -} -/** - * AppliedDiscountDTO represents an applied discount in a checkout - */ -export interface AppliedDiscountDTO { - id: number /* uint */; - code: string; - type: string; - method: string; - value: number /* float64 */; - amount: number /* float64 */; -} - -////////// -// source: email.go - -/** - * EmailTestDetails represents additional details in the email test response - */ -export interface EmailTestDetails { - target_email: string; - order_id: string; -} - -////////// -// source: order.go - -/** - * OrderDTO represents an order in the system - */ -export interface OrderDTO { - id: number /* uint */; - order_number: string; - user_id: number /* uint */; - checkout_id: string; - items: OrderItemDTO[]; - status: OrderStatus; - payment_status: PaymentStatus; - total_amount: number /* float64 */; // Subtotal (items only) - shipping_cost: number /* float64 */; // Shipping cost - discount_amount: number /* float64 */; // Discount applied amount - final_amount: number /* float64 */; // Total including shipping and discounts - currency: string; - shipping_address: AddressDTO; - billing_address: AddressDTO; - shipping_details: ShippingOptionDTO; - discount_details?: AppliedDiscountDTO; - payment_transactions?: PaymentTransactionDTO[]; - customer: CustomerDetailsDTO; - action_required: boolean; // Indicates if action is needed (e.g., payment) - action_url?: string; // URL for payment or order actions - created_at: string; - updated_at: string; -} -export interface OrderSummaryDTO { - id: number /* uint */; - order_number: string; - checkout_id: string; - user_id: number /* uint */; - customer: CustomerDetailsDTO; - status: OrderStatus; - payment_status: PaymentStatus; - total_amount: number /* float64 */; // Subtotal (items only) - shipping_cost: number /* float64 */; // Shipping cost - discount_amount: number /* float64 */; // Discount applied amount - final_amount: number /* float64 */; // Total including shipping and discounts - order_lines_amount: number /* int */; - currency: string; - created_at: string; - updated_at: string; -} -export interface PaymentDetails { - payment_id: string; - provider: PaymentProvider; - method: PaymentMethod; - status: string; - captured: boolean; - refunded: boolean; -} -/** - * OrderItemDTO represents an item in an order - */ -export interface OrderItemDTO { - id: number /* uint */; - order_id: number /* uint */; - product_id: number /* uint */; - variant_id?: number /* uint */; - sku: string; - product_name: string; - variant_name: string; - quantity: number /* int */; - unit_price: number /* float64 */; - total_price: number /* float64 */; - image_url: string; - created_at: string; - updated_at: string; -} -/** - * PaymentMethod represents the payment method used for an order - */ -export type PaymentMethod = string; -export const PaymentMethodCard: PaymentMethod = "credit_card"; -export const PaymentMethodWallet: PaymentMethod = "wallet"; -/** - * PaymentProvider represents the payment provider used for an order - */ -export type PaymentProvider = string; -export const PaymentProviderStripe: PaymentProvider = "stripe"; -export const PaymentProviderMobilePay: PaymentProvider = "mobilepay"; -/** - * OrderStatus represents the status of an order - */ -export type OrderStatus = string; -export const OrderStatusPending: OrderStatus = "pending"; -export const OrderStatusPaid: OrderStatus = "paid"; -export const OrderStatusShipped: OrderStatus = "shipped"; -export const OrderStatusCancelled: OrderStatus = "cancelled"; -export const OrderStatusCompleted: OrderStatus = "completed"; -/** - * PaymentStatus represents the status of a payment - */ -export type PaymentStatus = string; -export const PaymentStatusPending: PaymentStatus = "pending"; -export const PaymentStatusAuthorized: PaymentStatus = "authorized"; -export const PaymentStatusCaptured: PaymentStatus = "captured"; -export const PaymentStatusRefunded: PaymentStatus = "refunded"; -export const PaymentStatusCancelled: PaymentStatus = "cancelled"; -export const PaymentStatusFailed: PaymentStatus = "failed"; -/** - * PaymentTransactionDTO represents a payment transaction - */ -export interface PaymentTransactionDTO { - id: number /* uint */; - transaction_id: string; - external_id?: string; - type: TransactionType; - status: TransactionStatus; - amount: number /* float64 */; - currency: string; - provider: string; - created_at: string; - updated_at: string; -} -/** - * TransactionType represents the type of payment transaction - */ -export type TransactionType = string; -export const TransactionTypeAuthorize: TransactionType = "authorize"; -export const TransactionTypeCapture: TransactionType = "capture"; -export const TransactionTypeRefund: TransactionType = "refund"; -export const TransactionTypeCancel: TransactionType = "cancel"; -/** - * TransactionStatus represents the status of a payment transaction - */ -export type TransactionStatus = string; -export const TransactionStatusSuccessful: TransactionStatus = "successful"; -export const TransactionStatusFailed: TransactionStatus = "failed"; -export const TransactionStatusPending: TransactionStatus = "pending"; - -////////// -// source: product.go - -/** - * ProductDTO represents a product in the system - */ -export interface ProductDTO { - id: number /* uint */; - name: string; - description: string; - currency: string; - price: number /* float64 */; // Default variant price in given currency - sku: string; // Default variant SKU - total_stock: number /* int */; // Total stock across all variants - category: string; - category_id?: number /* uint */; - images: string[]; - has_variants: boolean; - active: boolean; - variants?: VariantDTO[]; - created_at: string; - updated_at: string; -} -/** - * VariantDTO represents a product variant - */ -export interface VariantDTO { - id: number /* uint */; - product_id: number /* uint */; - variant_name: string; - sku: string; - stock: number /* int */; - attributes: { [key: string]: string}; - images: string[]; - is_default: boolean; - weight: number /* float64 */; - price: number /* float64 */; - currency: string; - created_at: string; - updated_at: string; -} - -////////// -// source: shipping.go - -/** - * ShippingMethodDetailDTO represents a shipping method in the system with full details - */ -export interface ShippingMethodDetailDTO { - id: number /* uint */; - name: string; - description: string; - estimated_delivery_days: number /* int */; - active: boolean; - created_at: string; - updated_at: string; -} -/** - * ShippingZoneDTO represents a shipping zone in the system - */ -export interface ShippingZoneDTO { - id: number /* uint */; - name: string; - description: string; - countries: string[]; - active: boolean; - created_at: string; - updated_at: string; -} -/** - * ShippingRateDTO represents a shipping rate in the system - */ -export interface ShippingRateDTO { - id: number /* uint */; - shipping_method_id: number /* uint */; - shipping_method?: ShippingMethodDetailDTO; - shipping_zone_id: number /* uint */; - shipping_zone?: ShippingZoneDTO; - base_rate: number /* float64 */; - min_order_value: number /* float64 */; - free_shipping_threshold: number /* float64 */; - weight_based_rates?: WeightBasedRateDTO[]; - value_based_rates?: ValueBasedRateDTO[]; - active: boolean; - created_at: string; - updated_at: string; -} -/** - * WeightBasedRateDTO represents a weight-based rate in the system - */ -export interface WeightBasedRateDTO { - id: number /* uint */; - shipping_rate_id: number /* uint */; - min_weight: number /* float64 */; - max_weight: number /* float64 */; - rate: number /* float64 */; - created_at: string; - updated_at: string; -} -/** - * ValueBasedRateDTO represents a value-based rate in the system - */ -export interface ValueBasedRateDTO { - id: number /* uint */; - shipping_rate_id: number /* uint */; - min_order_value: number /* float64 */; - max_order_value: number /* float64 */; - rate: number /* float64 */; - created_at: string; - updated_at: string; -} -/** - * ShippingOptionDTO represents a shipping option with calculated cost - */ -export interface ShippingOptionDTO { - shipping_rate_id: number /* uint */; - shipping_method_id: number /* uint */; - name: string; - description: string; - estimated_delivery_days: number /* int */; - cost: number /* float64 */; - free_shipping: boolean; -} - -////////// -// source: user.go - -/** - * UserDTO represents a user in the system - */ -export interface UserDTO { - id: number /* uint */; - email: string; - first_name: string; - last_name: string; - role: string; - created_at: string; - updated_at: string; -}