2025-07-18 20:10:29 +07:00
package repository
import (
"context"
"time"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type AnalyticsRepository interface {
GetPaymentMethodAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] * entities . PaymentMethodAnalytics , error )
GetSalesAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ([] * entities . SalesAnalytics , error )
GetProductAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , limit int ) ([] * entities . ProductAnalytics , error )
2025-08-14 00:45:14 +07:00
GetProductAnalyticsPerCategory ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] * entities . ProductAnalyticsPerCategory , error )
2025-07-18 20:10:29 +07:00
GetDashboardOverview ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ( * entities . DashboardOverview , error )
GetProfitLossAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . ProfitLossAnalytics , error )
}
type AnalyticsRepositoryImpl struct {
db * gorm . DB
}
func NewAnalyticsRepositoryImpl ( db * gorm . DB ) * AnalyticsRepositoryImpl {
return & AnalyticsRepositoryImpl {
db : db ,
}
}
func ( r * AnalyticsRepositoryImpl ) GetPaymentMethodAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] * entities . PaymentMethodAnalytics , error ) {
var results [] * entities . PaymentMethodAnalytics
query := r . db . WithContext ( ctx ).
Table ( "payments p" ).
Select ( `
pm.id as payment_method_id,
pm.name as payment_method_name,
pm.type as payment_method_type,
COALESCE(SUM(p.amount), 0) as total_amount,
COUNT(DISTINCT p.order_id) as order_count,
COUNT(p.id) as payment_count
` ).
Joins ( "JOIN payment_methods pm ON p.payment_method_id = pm.id" ).
Joins ( "JOIN orders o ON p.order_id = o.id" ).
Where ( "o.organization_id = ?" , organizationID ).
2025-09-22 22:12:54 +07:00
Where ( "o.is_void = ?" , false ).
Where ( "o.is_refund = ?" , false ).
2025-07-18 20:10:29 +07:00
Where ( "p.status = ?" , entities . PaymentTransactionStatusCompleted ).
2025-09-22 22:12:54 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
2025-07-18 20:10:29 +07:00
if outletID != nil {
query = query . Where ( "o.outlet_id = ?" , * outletID )
}
err := query .
Group ( "pm.id, pm.name, pm.type" ).
Order ( "total_amount DESC" ).
Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) GetSalesAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ([] * entities . SalesAnalytics , error ) {
var results [] * entities . SalesAnalytics
var dateFormat string
switch groupBy {
case "hour" :
dateFormat = "DATE_TRUNC('hour', o.created_at)"
case "week" :
dateFormat = "DATE_TRUNC('week', o.created_at)"
case "month" :
dateFormat = "DATE_TRUNC('month', o.created_at)"
default :
dateFormat = "DATE(o.created_at)"
}
query := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
` + dateFormat + ` as date,
COALESCE(SUM(o.total_amount), 0) as sales,
COUNT(o.id) as orders,
2025-09-22 22:12:54 +07:00
COALESCE(SUM(CASE WHEN oi.status != 'cancelled' AND oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as items,
2025-07-18 20:10:29 +07:00
COALESCE(SUM(o.tax_amount), 0) as tax,
COALESCE(SUM(o.discount_amount), 0) as discount,
COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales
` ).
Joins ( "LEFT JOIN order_items oi ON o.id = oi.order_id" ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.is_void = ?" , false ).
2025-09-22 22:12:54 +07:00
Where ( "o.is_refund = ?" , false ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
2025-07-18 20:10:29 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
if outletID != nil {
query = query . Where ( "o.outlet_id = ?" , * outletID )
}
err := query .
Group ( "date" ).
Order ( "date ASC" ).
Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) GetProductAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , limit int ) ([] * entities . ProductAnalytics , error ) {
var results [] * entities . ProductAnalytics
2025-11-25 13:23:59 +07:00
query := r . db . WithContext ( ctx ).
Table ( "order_items oi" ).
2025-07-18 20:10:29 +07:00
Select ( `
p.id as product_id,
2025-11-25 13:23:59 +07:00
p.name as product_name,
c.id as category_id,
c.name as category_name,
c.order as category_order,
COALESCE(SUM(oi.quantity), 0) as quantity_sold,
COALESCE(SUM(oi.total_price), 0) as revenue,
2025-07-18 20:10:29 +07:00
CASE
2025-11-25 13:23:59 +07:00
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity)
2025-07-18 20:10:29 +07:00
ELSE 0
END as average_price,
COUNT(DISTINCT oi.order_id) as order_count
` ).
Joins ( "JOIN products p ON oi.product_id = p.id" ).
2025-11-25 13:23:59 +07:00
Joins ( "JOIN categories c ON p.category_id = c.id" ).
2025-07-18 20:10:29 +07:00
Joins ( "JOIN orders o ON oi.order_id = o.id" ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.is_void = ?" , false ).
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
if outletID != nil {
2025-11-25 13:23:59 +07:00
query = query . Where ( "o.outlet_id = ?" , * outletID )
2025-07-18 20:10:29 +07:00
}
err := query .
2025-11-25 13:23:59 +07:00
Group ( "p.id, p.name, c.id, c.name, c.order" ).
Order ( "revenue DESC" ).
2025-07-18 20:10:29 +07:00
Limit ( limit ).
Scan ( & results ). Error
return results , err
}
2025-08-14 00:45:14 +07:00
func ( r * AnalyticsRepositoryImpl ) GetProductAnalyticsPerCategory ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] * entities . ProductAnalyticsPerCategory , error ) {
var results [] * entities . ProductAnalyticsPerCategory
query := r . db . WithContext ( ctx ).
Table ( "order_items oi" ).
Select ( `
c.id as category_id,
c.name as category_name,
2025-09-22 22:12:54 +07:00
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
2025-08-14 00:45:14 +07:00
COUNT(DISTINCT p.id) as product_count,
COUNT(DISTINCT oi.order_id) as order_count
` ).
Joins ( "JOIN products p ON oi.product_id = p.id" ).
Joins ( "JOIN categories c ON p.category_id = c.id" ).
Joins ( "JOIN orders o ON oi.order_id = o.id" ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.is_void = ?" , false ).
2025-09-22 22:12:54 +07:00
Where ( "o.is_refund = ?" , false ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
Where ( "oi.status != ?" , entities . OrderItemStatusCancelled ).
2025-08-14 00:45:14 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
if outletID != nil {
query = query . Where ( "o.outlet_id = ?" , * outletID )
}
err := query .
Group ( "c.id, c.name" ).
2025-09-25 20:31:57 +07:00
Order ( "c.name ASC" ).
2025-08-14 00:45:14 +07:00
Scan ( & results ). Error
return results , err
}
2025-07-18 20:10:29 +07:00
func ( r * AnalyticsRepositoryImpl ) GetDashboardOverview ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ( * entities . DashboardOverview , error ) {
var result entities . DashboardOverview
query := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
2025-10-15 18:55:35 +07:00
COALESCE(SUM(CASE WHEN o.is_void = false AND o.is_refund = false AND o.payment_status = 'completed' THEN o.total_amount ELSE 0 END), 0) as total_sales,
2025-07-18 20:10:29 +07:00
COUNT(CASE WHEN o.is_void = false THEN o.id END) as total_orders,
CASE
WHEN COUNT(CASE WHEN o.is_void = false THEN o.id END) > 0
THEN COALESCE(SUM(CASE WHEN o.is_void = false THEN o.total_amount ELSE 0 END), 0) / COUNT(CASE WHEN o.is_void = false THEN o.id END)
ELSE 0
END as average_order_value,
COUNT(DISTINCT o.customer_id) as total_customers,
COUNT(CASE WHEN o.is_void = true THEN o.id END) as voided_orders,
COUNT(CASE WHEN o.is_refund = true THEN o.id END) as refunded_orders
` ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
if outletID != nil {
query = query . Where ( "o.outlet_id = ?" , * outletID )
}
err := query . Scan ( & result ). Error
if err != nil {
return nil , err
}
return & result , nil
}
func ( r * AnalyticsRepositoryImpl ) GetProfitLossAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . ProfitLossAnalytics , error ) {
// Summary query
var summary entities . ProfitLossSummary
summaryQuery := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
COALESCE(SUM(o.total_amount), 0) as total_revenue,
COALESCE(SUM(o.total_cost), 0) as total_cost,
COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit,
CASE
WHEN SUM(o.total_amount) > 0
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100
ELSE 0
END as gross_profit_margin,
COALESCE(SUM(o.tax_amount), 0) as total_tax,
COALESCE(SUM(o.discount_amount), 0) as total_discount,
COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit,
CASE
WHEN SUM(o.total_amount) > 0
THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100
ELSE 0
END as net_profit_margin,
COUNT(o.id) as total_orders,
CASE
WHEN COUNT(o.id) > 0
THEN SUM(o.total_amount - o.total_cost - o.discount_amount) / COUNT(o.id)
ELSE 0
END as average_profit,
CASE
WHEN SUM(o.total_cost) > 0
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_cost)) * 100
ELSE 0
END as profitability_ratio
` ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.status = ?" , entities . OrderStatusCompleted ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
Where ( "o.is_void = false AND o.is_refund = false" ).
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
if outletID != nil {
summaryQuery = summaryQuery . Where ( "o.outlet_id = ?" , * outletID )
}
err := summaryQuery . Scan ( & summary ). Error
if err != nil {
return nil , err
}
// Time series data query
var timeFormat string
switch groupBy {
case "hour" :
timeFormat = "DATE_TRUNC('hour', o.created_at)"
case "week" :
timeFormat = "DATE_TRUNC('week', o.created_at)"
case "month" :
timeFormat = "DATE_TRUNC('month', o.created_at)"
default : // day
timeFormat = "DATE_TRUNC('day', o.created_at)"
}
var data [] entities . ProfitLossData
dataQuery := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
` + timeFormat + ` as date,
COALESCE(SUM(o.total_amount), 0) as revenue,
COALESCE(SUM(o.total_cost), 0) as cost,
COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit,
CASE
WHEN SUM(o.total_amount) > 0
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100
ELSE 0
END as gross_profit_margin,
COALESCE(SUM(o.tax_amount), 0) as tax,
COALESCE(SUM(o.discount_amount), 0) as discount,
COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit,
CASE
WHEN SUM(o.total_amount) > 0
THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100
ELSE 0
END as net_profit_margin,
COUNT(o.id) as orders
` ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.status = ?" , entities . OrderStatusCompleted ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
Where ( "o.is_void = false AND o.is_refund = false" ).
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo ).
Group ( timeFormat ).
Order ( timeFormat )
if outletID != nil {
dataQuery = dataQuery . Where ( "o.outlet_id = ?" , * outletID )
}
err = dataQuery . Scan ( & data ). Error
if err != nil {
return nil , err
}
// Product profit data query
var productData [] entities . ProductProfitData
productQuery := r . db . WithContext ( ctx ).
Table ( "order_items oi" ).
Select ( `
p.id as product_id,
p.name as product_name,
c.id as category_id,
c.name as category_name,
2025-09-22 22:12:54 +07:00
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) as quantity_sold,
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) as revenue,
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END) as cost,
SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) as gross_profit,
2025-07-18 20:10:29 +07:00
CASE
2025-09-22 22:12:54 +07:00
WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) > 0
THEN (SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)) * 100
2025-07-18 20:10:29 +07:00
ELSE 0
END as gross_profit_margin,
2025-09-22 22:12:54 +07:00
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price ELSE NULL END) as average_price,
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_cost ELSE NULL END) as average_cost,
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price - oi.unit_cost ELSE NULL END) as profit_per_unit
2025-07-18 20:10:29 +07:00
` ).
Joins ( "JOIN orders o ON oi.order_id = o.id" ).
Joins ( "JOIN products p ON oi.product_id = p.id" ).
Joins ( "JOIN categories c ON p.category_id = c.id" ).
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.status = ?" , entities . OrderStatusCompleted ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
Where ( "o.is_void = false AND o.is_refund = false" ).
2025-09-22 22:12:54 +07:00
Where ( "oi.status != ?" , entities . OrderItemStatusCancelled ).
2025-07-18 20:10:29 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo ).
Group ( "p.id, p.name, c.id, c.name" ).
2025-09-25 20:31:57 +07:00
Order ( "p.name ASC" ).
2025-09-22 22:12:54 +07:00
Limit ( 1000 )
2025-07-18 20:10:29 +07:00
if outletID != nil {
productQuery = productQuery . Where ( "o.outlet_id = ?" , * outletID )
}
err = productQuery . Scan ( & productData ). Error
if err != nil {
return nil , err
}
return & entities . ProfitLossAnalytics {
Summary : summary ,
Data : data ,
ProductData : productData ,
}, nil
}