Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ go.work.sum
*.sqlite3

bin/
commercify
commercify

web/types/
12 changes: 5 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,12 +119,14 @@ 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
go run ./cmd/expire-checkouts

force-delete-checkouts: ## Force delete all expired, abandoned, and old completed checkouts
go run ./cmd/expire-checkouts -force


156 changes: 156 additions & 0 deletions docs/dashboard_api_examples.md
Original file line number Diff line number Diff line change
@@ -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"
174 changes: 174 additions & 0 deletions internal/application/usecase/dashboard_usecase.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading