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 )
2026-05-19 14:45:26 +07:00
GetPurchasingAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . PurchasingAnalytics , error )
2025-07-18 20:10:29 +07:00
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 )
2026-06-01 13:13:40 +07:00
GetProfitLossAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . ProfitLossAnalytics , error )
2025-07-18 20:10:29 +07:00
}
type AnalyticsRepositoryImpl struct {
db * gorm . DB
}
func NewAnalyticsRepositoryImpl ( db * gorm . DB ) * AnalyticsRepositoryImpl {
return & AnalyticsRepositoryImpl {
db : db ,
}
}
2026-05-12 21:47:35 +07:00
func ( r * AnalyticsRepositoryImpl ) resolveOutletID ( query * gorm . DB , outletID * uuid . UUID , column string ) * gorm . DB {
if outletID != nil {
return query . Where ( column + " = ?" , * outletID )
}
return query
}
2025-07-18 20:10:29 +07:00
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
2026-05-12 21:47:35 +07:00
query = r . resolveOutletID ( query , outletID , "o.outlet_id" )
2025-07-18 20:10:29 +07:00
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)"
}
2026-04-27 13:24:16 +07:00
outletFilter := ""
args := [] interface {}{ organizationID , false , false , string ( entities . PaymentStatusCompleted ), dateFrom , dateTo }
if outletID != nil {
outletFilter = "AND o.outlet_id = ?"
args = append ( args , * outletID )
}
rawQuery := `
SELECT
` + dateFormat + ` as date,
2025-07-18 20:10:29 +07:00
COALESCE(SUM(o.total_amount), 0) as sales,
COUNT(o.id) as orders,
2026-04-27 13:24:16 +07:00
COALESCE(SUM(oi_agg.total_items), 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
2026-04-27 13:24:16 +07:00
FROM orders o
LEFT JOIN (
SELECT
oi.order_id,
SUM(oi.quantity - COALESCE(oi.refund_quantity, 0)) as total_items
FROM order_items oi
WHERE oi.status != 'cancelled'
AND oi.is_fully_refunded = false
GROUP BY oi.order_id
) oi_agg ON oi_agg.order_id = o.id
WHERE o.organization_id = ?
AND o.is_void = ?
AND o.is_refund = ?
AND o.payment_status = ?
AND o.created_at >= ? AND o.created_at <= ?
` + outletFilter + `
GROUP BY date
ORDER BY date ASC
`
2025-07-18 20:10:29 +07:00
2026-04-27 13:24:16 +07:00
err := r . db . WithContext ( ctx ). Raw ( rawQuery , args ... ). Scan ( & results ). Error
2025-07-18 20:10:29 +07:00
return results , err
}
2026-05-19 14:45:26 +07:00
func ( r * AnalyticsRepositoryImpl ) GetPurchasingAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . PurchasingAnalytics , error ) {
var summary entities . PurchasingSummary
var outletName * string
if outletID != nil {
var outlet struct {
Name string
}
result := r . db . WithContext ( ctx ).
Table ( "outlets" ).
Select ( "name" ).
Where ( "id = ? AND organization_id = ?" , * outletID , organizationID ).
Limit ( 1 ).
Scan ( & outlet )
if result . Error != nil {
return nil , result . Error
}
if result . RowsAffected > 0 {
outletName = & outlet . Name
}
}
summaryQuery := r . db . WithContext ( ctx ).
Table ( "inventory_movements im" ).
Select ( `
COALESCE(SUM(im.total_cost), 0) as total_purchases,
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
COALESCE(SUM(im.quantity), 0) as total_quantity,
CASE
WHEN COUNT(DISTINCT im.reference_id) > 0
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
ELSE 0
END as average_purchase_order_value,
COUNT(DISTINCT im.item_id) as total_ingredients,
COUNT(DISTINCT po.vendor_id) as total_vendors
` ).
Joins ( "LEFT JOIN purchase_orders po ON im.reference_id = po.id" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.movement_type = ?" , entities . InventoryMovementTypePurchase ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.reference_type = ?" , entities . InventoryMovementReferenceTypePurchaseOrder ).
Where ( "im.created_at >= ? AND im.created_at <= ?" , dateFrom , dateTo )
summaryQuery = r . resolveOutletID ( summaryQuery , outletID , "im.outlet_id" )
if err := summaryQuery . Scan ( & summary ). Error ; err != nil {
return nil , err
}
var dateFormat string
switch groupBy {
case "hour" :
dateFormat = "DATE_TRUNC('hour', im.created_at)"
case "week" :
dateFormat = "DATE_TRUNC('week', im.created_at)"
case "month" :
dateFormat = "DATE_TRUNC('month', im.created_at)"
default :
dateFormat = "DATE_TRUNC('day', im.created_at)"
}
var data [] entities . PurchasingAnalyticsData
dataQuery := r . db . WithContext ( ctx ).
Table ( "inventory_movements im" ).
Select ( `
` + dateFormat + ` as date,
COALESCE(SUM(im.total_cost), 0) as purchases,
COUNT(DISTINCT im.reference_id) as purchase_orders,
COALESCE(SUM(im.quantity), 0) as quantity,
COUNT(DISTINCT im.item_id) as ingredients,
COUNT(DISTINCT po.vendor_id) as vendors
` ).
Joins ( "LEFT JOIN purchase_orders po ON im.reference_id = po.id" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.movement_type = ?" , entities . InventoryMovementTypePurchase ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.reference_type = ?" , entities . InventoryMovementReferenceTypePurchaseOrder ).
Where ( "im.created_at >= ? AND im.created_at <= ?" , dateFrom , dateTo ).
Group ( dateFormat ).
Order ( dateFormat )
dataQuery = r . resolveOutletID ( dataQuery , outletID , "im.outlet_id" )
if err := dataQuery . Scan ( & data ). Error ; err != nil {
return nil , err
}
var ingredientData [] entities . PurchasingIngredientData
ingredientQuery := r . db . WithContext ( ctx ).
Table ( "inventory_movements im" ).
Select ( `
i.id as ingredient_id,
i.name as ingredient_name,
COALESCE(SUM(im.quantity), 0) as quantity,
COALESCE(SUM(im.total_cost), 0) as total_cost,
CASE
WHEN SUM(im.quantity) > 0
THEN COALESCE(SUM(im.total_cost), 0) / SUM(im.quantity)
ELSE 0
END as average_unit_cost,
COUNT(DISTINCT im.reference_id) as purchase_order_count
` ).
Joins ( "JOIN ingredients i ON im.item_id = i.id" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.movement_type = ?" , entities . InventoryMovementTypePurchase ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.reference_type = ?" , entities . InventoryMovementReferenceTypePurchaseOrder ).
Where ( "im.created_at >= ? AND im.created_at <= ?" , dateFrom , dateTo ).
Group ( "i.id, i.name" ).
Order ( "total_cost DESC" )
ingredientQuery = r . resolveOutletID ( ingredientQuery , outletID , "im.outlet_id" )
if err := ingredientQuery . Scan ( & ingredientData ). Error ; err != nil {
return nil , err
}
var vendorData [] entities . PurchasingVendorData
vendorQuery := r . db . WithContext ( ctx ).
Table ( "inventory_movements im" ).
Select ( `
v.id as vendor_id,
v.name as vendor_name,
COALESCE(SUM(im.total_cost), 0) as total_cost,
COUNT(DISTINCT im.reference_id) as purchase_order_count,
COUNT(DISTINCT im.item_id) as ingredient_count,
COALESCE(SUM(im.quantity), 0) as quantity
` ).
Joins ( "JOIN purchase_orders po ON im.reference_id = po.id" ).
Joins ( "JOIN vendors v ON po.vendor_id = v.id" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.movement_type = ?" , entities . InventoryMovementTypePurchase ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.reference_type = ?" , entities . InventoryMovementReferenceTypePurchaseOrder ).
Where ( "im.created_at >= ? AND im.created_at <= ?" , dateFrom , dateTo ).
Group ( "v.id, v.name" ).
Order ( "total_cost DESC" )
vendorQuery = r . resolveOutletID ( vendorQuery , outletID , "im.outlet_id" )
if err := vendorQuery . Scan ( & vendorData ). Error ; err != nil {
return nil , err
}
return & entities . PurchasingAnalytics {
OutletName : outletName ,
Summary : summary ,
Data : data ,
IngredientData : ingredientData ,
VendorData : vendorData ,
}, nil
}
2025-07-18 20:10:29 +07:00
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,
2026-04-24 17:56:38 +07:00
COUNT(DISTINCT oi.order_id) as order_count,
COALESCE((
SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost)
FROM product_recipes pr
JOIN ingredients i ON pr.ingredient_id = i.id
WHERE pr.product_id = p.id
), p.cost, 0) as standard_hpp_per_unit,
COALESCE((
SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost)
FROM product_recipes pr
JOIN ingredients i ON pr.ingredient_id = i.id
WHERE pr.product_id = p.id
), p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as standard_hpp_total,
CASE
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_cost), 0) / SUM(oi.quantity)
ELSE 0
END as fifo_hpp_per_unit,
COALESCE(SUM(oi.total_cost), 0) as fifo_hpp_total,
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as moving_average_hpp_total
2025-07-18 20:10:29 +07:00
` ).
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" ).
2026-04-24 17:56:38 +07:00
Joins ( "LEFT JOIN (?) mahpp ON mahpp.product_id = p.id" ,
r . db . Table ( "product_recipes pr2" ).
Select ( "pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit" ).
Joins ( "JOIN ingredients ing ON pr2.ingredient_id = ing.id" ).
Joins ( "LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id" ,
r . db . Table ( "inventory_movements im" ).
Select ( "im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost" ).
Where ( "im.movement_type = ?" , "purchase" ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.created_at <= ?" , dateTo ).
Group ( "im.item_id" ),
).
Group ( "pr2.product_id" ),
).
2025-07-18 20:10:29 +07:00
Where ( "o.organization_id = ?" , organizationID ).
Where ( "o.is_void = ?" , false ).
2025-11-25 21:44:20 +07:00
Where ( "o.is_refund = ?" , false ).
Where ( "o.payment_status = ?" , entities . PaymentStatusCompleted ).
Where ( "oi.status != ?" , entities . OrderItemStatusCancelled ).
2025-07-18 20:10:29 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo )
2026-05-12 21:47:35 +07:00
query = r . resolveOutletID ( query , outletID , "o.outlet_id" )
2025-07-18 20:10:29 +07:00
err := query .
2026-04-24 17:56:38 +07:00
Group ( "p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit" ).
2025-11-25 13:23:59 +07:00
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,
2026-04-24 17:56:38 +07:00
COUNT(DISTINCT oi.order_id) as order_count,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
COALESCE(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), 0) as total_fifo_hpp,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
2025-08-14 00:45:14 +07:00
` ).
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" ).
2026-04-24 17:56:38 +07:00
Joins ( "LEFT JOIN (SELECT pr.product_id, SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) as hpp_per_unit FROM product_recipes pr JOIN ingredients i ON pr.ingredient_id = i.id GROUP BY pr.product_id) shpp ON shpp.product_id = p.id" ).
Joins ( "LEFT JOIN (?) mahpp ON mahpp.product_id = p.id" ,
r . db . Table ( "product_recipes pr2" ).
Select ( "pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit" ).
Joins ( "JOIN ingredients ing ON pr2.ingredient_id = ing.id" ).
Joins ( "LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id" ,
r . db . Table ( "inventory_movements im" ).
Select ( "im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost" ).
Where ( "im.movement_type = ?" , "purchase" ).
Where ( "im.item_type = ?" , "INGREDIENT" ).
Where ( "im.organization_id = ?" , organizationID ).
Where ( "im.created_at <= ?" , dateTo ).
Group ( "im.item_id" ),
).
Group ( "pr2.product_id" ),
).
2025-08-14 00:45:14 +07:00
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 )
2026-05-12 21:47:35 +07:00
query = r . resolveOutletID ( query , outletID , "o.outlet_id" )
2025-08-14 00:45:14 +07:00
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 )
2026-05-12 21:47:35 +07:00
query = r . resolveOutletID ( query , outletID , "o.outlet_id" )
2025-07-18 20:10:29 +07:00
err := query . Scan ( & result ). Error
if err != nil {
return nil , err
}
return & result , nil
}
2026-06-01 13:13:40 +07:00
func ( r * AnalyticsRepositoryImpl ) GetProfitLossAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time , groupBy string ) ( * entities . ProfitLossAnalytics , error ) {
2026-05-29 18:24:14 +07:00
mtdStart := time . Date ( dateTo . Year (), dateTo . Month (), 1 , 0 , 0 , 0 , 0 , dateTo . Location ())
todayStart := time . Date ( dateTo . Year (), dateTo . Month (), dateTo . Day (), 0 , 0 , 0 , 0 , dateTo . Location ())
2026-05-26 14:59:56 +07:00
todayEnd := todayStart . Add ( 24 * time . Hour ). Add ( - time . Nanosecond )
2025-07-18 20:10:29 +07:00
2026-06-01 13:13:40 +07:00
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 )
summaryQuery = r . resolveOutletID ( summaryQuery , outletID , "o.outlet_id" )
if err := summaryQuery . Scan ( & summary ). Error ; err != nil {
return nil , err
}
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 :
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 )
dataQuery = r . resolveOutletID ( dataQuery , outletID , "o.outlet_id" )
if err := dataQuery . Scan ( & data ). Error ; err != nil {
return nil , err
}
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,
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,
CASE
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
ELSE 0
END as gross_profit_margin,
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
` ).
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" ).
Where ( "oi.status != ?" , entities . OrderItemStatusCancelled ).
Where ( "o.created_at >= ? AND o.created_at <= ?" , dateFrom , dateTo ).
Group ( "p.id, p.name, c.id, c.name" ).
Order ( "p.name ASC" ).
Limit ( 1000 )
productQuery = r . resolveOutletID ( productQuery , outletID , "o.outlet_id" )
if err := productQuery . Scan ( & productData ). Error ; err != nil {
return nil , err
}
2026-05-26 14:59:56 +07:00
type revenueCostResult struct {
Revenue float64
Cost float64
2025-07-18 20:10:29 +07:00
}
2026-05-26 14:59:56 +07:00
var todayRC revenueCostResult
todayQuery := r . db . WithContext ( ctx ).
2025-07-18 20:10:29 +07:00
Table ( "orders o" ).
Select ( `
COALESCE(SUM(o.total_amount), 0) as revenue,
2026-05-26 14:59:56 +07:00
COALESCE(SUM(o.total_cost), 0) as cost
2025-07-18 20:10:29 +07:00
` ).
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" ).
2026-05-26 14:59:56 +07:00
Where ( "o.created_at >= ? AND o.created_at <= ?" , todayStart , todayEnd )
todayQuery = r . resolveOutletID ( todayQuery , outletID , "o.outlet_id" )
if err := todayQuery . Scan ( & todayRC ). Error ; err != nil {
return nil , err
}
2025-07-18 20:10:29 +07:00
2026-05-26 14:59:56 +07:00
var mtdRC revenueCostResult
mtdQuery := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
COALESCE(SUM(o.total_amount), 0) as revenue,
COALESCE(SUM(o.total_cost), 0) as cost
` ).
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 <= ?" , mtdStart , todayEnd )
mtdQuery = r . resolveOutletID ( mtdQuery , outletID , "o.outlet_id" )
if err := mtdQuery . Scan ( & mtdRC ). Error ; err != nil {
return nil , err
}
2025-07-18 20:10:29 +07:00
2026-05-26 14:59:56 +07:00
todayExpenseByCategory , err := r . getExpenseByCategory ( ctx , organizationID , outletID , todayStart , todayEnd )
2025-07-18 20:10:29 +07:00
if err != nil {
return nil , err
}
2026-05-26 14:59:56 +07:00
mtdExpenseByCategory , err := r . getExpenseByCategory ( ctx , organizationID , outletID , mtdStart , todayEnd )
if err != nil {
return nil , err
}
2025-07-18 20:10:29 +07:00
2026-05-26 14:59:56 +07:00
opsItems , err := r . getOperationalExpenseItems ( ctx , organizationID , outletID , mtdStart , todayEnd )
2025-07-18 20:10:29 +07:00
if err != nil {
return nil , err
}
return & entities . ProfitLossAnalytics {
2026-06-01 13:13:40 +07:00
Summary : summary ,
Data : data ,
ProductData : productData ,
2026-05-26 14:59:56 +07:00
TodayRevenue : todayRC . Revenue ,
TodayCost : todayRC . Cost ,
MtdRevenue : mtdRC . Revenue ,
MtdCost : mtdRC . Cost ,
TodayExpenseByCategory : todayExpenseByCategory ,
MtdExpenseByCategory : mtdExpenseByCategory ,
OperationalExpenseItems : opsItems ,
2025-07-18 20:10:29 +07:00
}, nil
}
2026-05-26 14:59:56 +07:00
func ( r * AnalyticsRepositoryImpl ) getExpenseByCategory ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . ExpenseCategoryTotal , error ) {
var results [] entities . ExpenseCategoryTotal
query := r . db . WithContext ( ctx ).
Table ( "expense_items ei" ).
2026-06-03 13:00:50 +07:00
Select ( `COALESCE(parent_coa.name, coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount` ).
2026-05-26 14:59:56 +07:00
Joins ( "JOIN expenses e ON ei.expense_id = e.id" ).
Joins ( "JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id" ).
Joins ( "LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id" ).
Where ( "e.organization_id = ?" , organizationID ).
2026-05-29 15:44:59 +07:00
Where ( "e.status = ?" , "approved" ).
2026-05-26 14:59:56 +07:00
Where ( "e.transaction_date >= ? AND e.transaction_date <= ?" , dateFrom , dateTo )
if outletID != nil {
query = query . Where ( "e.outlet_id = ?" , * outletID )
}
err := query .
2026-06-03 13:00:50 +07:00
Group ( "COALESCE(parent_coa.name, coa.name, 'Lain-lain')" ).
Order ( "COALESCE(parent_coa.name, coa.name, 'Lain-lain')" ).
2026-05-26 14:59:56 +07:00
Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) getOperationalExpenseItems ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . OperationalExpenseItem , error ) {
var results [] entities . OperationalExpenseItem
query := r . db . WithContext ( ctx ).
Table ( "expense_items ei" ).
2026-05-29 13:25:38 +07:00
Select ( `COALESCE(NULLIF(ei.item, ''), ei.description, coa.name) as item, COALESCE(SUM(ei.amount), 0) as amount` ).
2026-05-26 14:59:56 +07:00
Joins ( "JOIN expenses e ON ei.expense_id = e.id" ).
Joins ( "JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id" ).
Where ( "e.organization_id = ?" , organizationID ).
2026-05-29 15:44:59 +07:00
Where ( "e.status = ?" , "approved" ).
2026-05-26 14:59:56 +07:00
Where ( "e.transaction_date >= ? AND e.transaction_date <= ?" , dateFrom , dateTo )
if outletID != nil {
query = query . Where ( "e.outlet_id = ?" , * outletID )
}
err := query .
2026-05-29 13:25:38 +07:00
Group ( "COALESCE(NULLIF(ei.item, ''), ei.description, coa.name)" ).
2026-05-26 14:59:56 +07:00
Order ( "amount DESC" ).
Scan ( & results ). Error
return results , err
}