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 )
2026-06-17 18:17:08 +07:00
GetExclusiveSummaryAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ( * entities . ExclusiveSummaryAnalytics , error )
2026-06-17 18:26:40 +07:00
GetExclusiveSummaryBankBalances ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID ) ([] entities . ExclusiveSummaryBankBalance , 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 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
}
}
2026-06-11 16:47:03 +07:00
return r . getPurchaseOrderPurchasingAnalytics ( ctx , organizationID , outletID , outletName , dateFrom , dateTo , groupBy )
}
2026-05-19 14:45:26 +07:00
2026-06-11 16:47:03 +07:00
func ( r * AnalyticsRepositoryImpl ) getPurchaseOrderPurchasingAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , outletName * string , dateFrom , dateTo time . Time , groupBy string ) ( * entities . PurchasingAnalytics , error ) {
var summary entities . PurchasingSummary
2026-06-11 15:57:19 +07:00
summaryQuery := r . db . WithContext ( ctx ).
2026-06-11 16:47:03 +07:00
Table ( "purchase_orders po" ).
2026-06-11 15:57:19 +07:00
Select ( `
2026-06-11 16:47:03 +07:00
COALESCE(SUM(poi.amount), 0) as total_purchases,
COUNT(DISTINCT po.id) as total_purchase_orders,
COALESCE(SUM(poi.quantity), 0) as total_quantity,
2026-05-19 14:45:26 +07:00
CASE
2026-06-11 16:47:03 +07:00
WHEN COUNT(DISTINCT po.id) > 0
THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id)
2026-05-19 14:45:26 +07:00
ELSE 0
END as average_purchase_order_value,
2026-06-11 16:47:03 +07:00
COUNT(DISTINCT i.id) as total_ingredients,
2026-06-18 11:04:59 +07:00
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as total_vendors
2026-06-11 15:57:19 +07:00
` ).
2026-06-11 16:47:03 +07:00
Joins ( "LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id" ).
Joins ( "LEFT JOIN ingredients i ON poi.ingredient_id = i.id" ).
Joins ( "LEFT JOIN units u ON poi.unit_id = u.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "po.status != ?" , "cancelled" ).
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo )
summaryQuery = r . applyPurchaseOrderItemOutletFilter ( summaryQuery , outletID )
2026-06-11 15:57:19 +07:00
if err := summaryQuery . Scan ( & summary ). Error ; err != nil {
2026-05-19 14:45:26 +07:00
return nil , err
}
var dateFormat string
switch groupBy {
case "hour" :
2026-06-11 16:47:03 +07:00
dateFormat = "DATE_TRUNC('hour', po.created_at)"
2026-05-19 14:45:26 +07:00
case "week" :
2026-06-11 16:47:03 +07:00
dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)"
2026-05-19 14:45:26 +07:00
case "month" :
2026-06-11 16:47:03 +07:00
dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)"
2026-05-19 14:45:26 +07:00
default :
2026-06-11 16:47:03 +07:00
dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)"
2026-05-19 14:45:26 +07:00
}
var data [] entities . PurchasingAnalyticsData
2026-06-11 15:57:19 +07:00
dataQuery := r . db . WithContext ( ctx ).
2026-06-11 16:47:03 +07:00
Table ( "purchase_orders po" ).
2026-06-11 15:57:19 +07:00
Select ( `
` + dateFormat + ` as date,
2026-06-11 16:47:03 +07:00
COALESCE(SUM(poi.amount), 0) as purchases,
COUNT(DISTINCT po.id) as purchase_orders,
COALESCE(SUM(poi.quantity), 0) as quantity,
COUNT(DISTINCT i.id) as ingredients,
2026-06-18 11:04:59 +07:00
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as vendors
2026-06-11 15:57:19 +07:00
` ).
2026-06-11 16:47:03 +07:00
Joins ( "LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id" ).
Joins ( "LEFT JOIN ingredients i ON poi.ingredient_id = i.id" ).
Joins ( "LEFT JOIN units u ON poi.unit_id = u.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "po.status != ?" , "cancelled" ).
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo ).
2026-06-11 15:57:19 +07:00
Group ( dateFormat ).
Order ( dateFormat )
2026-06-11 16:47:03 +07:00
dataQuery = r . applyPurchaseOrderItemOutletFilter ( dataQuery , outletID )
2026-06-11 15:57:19 +07:00
if err := dataQuery . Scan ( & data ). Error ; err != nil {
2026-05-19 14:45:26 +07:00
return nil , err
}
var ingredientData [] entities . PurchasingIngredientData
ingredientQuery := r . db . WithContext ( ctx ).
2026-06-11 16:47:03 +07:00
Table ( "purchase_order_items poi" ).
2026-05-19 14:45:26 +07:00
Select ( `
i.id as ingredient_id,
i.name as ingredient_name,
2026-06-11 16:47:03 +07:00
COALESCE(SUM(poi.quantity), 0) as quantity,
COALESCE(SUM(poi.amount), 0) as total_cost,
2026-05-19 14:45:26 +07:00
CASE
2026-06-11 16:47:03 +07:00
WHEN SUM(poi.quantity) > 0
THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity)
2026-05-19 14:45:26 +07:00
ELSE 0
END as average_unit_cost,
2026-06-11 16:47:03 +07:00
COUNT(DISTINCT po.id) as purchase_order_count
2026-05-19 14:45:26 +07:00
` ).
2026-06-11 16:47:03 +07:00
Joins ( "JOIN purchase_orders po ON poi.purchase_order_id = po.id" ).
Joins ( "JOIN ingredients i ON poi.ingredient_id = i.id" ).
Joins ( "LEFT JOIN units u ON poi.unit_id = u.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "po.status != ?" , "cancelled" ).
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo ).
2026-05-19 14:45:26 +07:00
Group ( "i.id, i.name" ).
Order ( "total_cost DESC" )
2026-06-11 16:47:03 +07:00
ingredientQuery = r . applyPurchaseOrderItemOutletFilter ( ingredientQuery , outletID )
2026-05-19 14:45:26 +07:00
if err := ingredientQuery . Scan ( & ingredientData ). Error ; err != nil {
return nil , err
}
var vendorData [] entities . PurchasingVendorData
vendorQuery := r . db . WithContext ( ctx ).
2026-06-11 16:47:03 +07:00
Table ( "purchase_orders po" ).
2026-05-19 14:45:26 +07:00
Select ( `
v.id as vendor_id,
2026-06-18 11:04:59 +07:00
COALESCE(v.name, 'No Vendor') as vendor_name,
2026-06-11 16:47:03 +07:00
COALESCE(SUM(poi.amount), 0) as total_cost,
COUNT(DISTINCT po.id) as purchase_order_count,
COUNT(DISTINCT i.id) as ingredient_count,
COALESCE(SUM(poi.quantity), 0) as quantity
2026-05-19 14:45:26 +07:00
` ).
2026-06-18 11:04:59 +07:00
Joins ( "LEFT JOIN vendors v ON po.vendor_id = v.id" ).
2026-06-11 16:47:03 +07:00
Joins ( "LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id" ).
Joins ( "LEFT JOIN ingredients i ON poi.ingredient_id = i.id" ).
Joins ( "LEFT JOIN units u ON poi.unit_id = u.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "po.status != ?" , "cancelled" ).
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo ).
2026-06-18 11:04:59 +07:00
Group ( "v.id, COALESCE(v.name, 'No Vendor')" ).
2026-05-19 14:45:26 +07:00
Order ( "total_cost DESC" )
2026-06-11 16:47:03 +07:00
vendorQuery = r . applyPurchaseOrderItemOutletFilter ( vendorQuery , outletID )
2026-05-19 14:45:26 +07:00
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
}
2026-06-11 16:47:03 +07:00
func ( r * AnalyticsRepositoryImpl ) applyPurchaseOrderItemOutletFilter ( query * gorm . DB , outletID * uuid . UUID ) * gorm . DB {
if outletID == nil {
return query
}
2026-06-18 15:27:20 +07:00
return query . Where ( "po.outlet_id = ?" , * outletID )
2026-06-11 16:47:03 +07:00
}
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-06-11 16:47:03 +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-11 16:47:03 +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-11 16:47:03 +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
}
2026-06-17 18:17:08 +07:00
func ( r * AnalyticsRepositoryImpl ) GetExclusiveSummaryAnalytics ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ( * entities . ExclusiveSummaryAnalytics , error ) {
type salesResult struct {
SalesTotal float64
SalesCount int64
}
var sales salesResult
salesQuery := r . db . WithContext ( ctx ).
Table ( "orders o" ).
Select ( `
COALESCE(SUM(o.total_amount), 0) as sales_total,
COUNT(o.id) as sales_count
` ).
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 )
salesQuery = r . resolveOutletID ( salesQuery , outletID , "o.outlet_id" )
if err := salesQuery . Scan ( & sales ). Error ; err != nil {
return nil , err
}
hppBreakdown , err := r . getExclusiveSummaryHPPBreakdown ( ctx , organizationID , outletID , dateFrom , dateTo )
if err != nil {
return nil , err
}
2026-06-18 15:27:20 +07:00
operationalExpenseBreakdown , err := r . getExclusiveSummaryOperationalExpenseBreakdown ( ctx , organizationID , outletID , dateFrom , dateTo )
2026-06-17 18:17:08 +07:00
if err != nil {
return nil , err
}
dailySummary , err := r . getExclusiveSummaryDailySummary ( ctx , organizationID , outletID , dateFrom , dateTo )
if err != nil {
return nil , err
}
dailyTransactions , err := r . getExclusiveSummaryDailyTransactions ( ctx , organizationID , outletID , dateFrom , dateTo )
if err != nil {
return nil , err
}
return & entities . ExclusiveSummaryAnalytics {
SalesTotal : sales . SalesTotal ,
SalesCount : sales . SalesCount ,
HPPBreakdown : hppBreakdown ,
OperationalExpenseBreakdown : operationalExpenseBreakdown ,
DailySummary : dailySummary ,
DailyTransactions : dailyTransactions ,
}, nil
}
func ( r * AnalyticsRepositoryImpl ) getExclusiveSummaryHPPBreakdown ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . ExclusiveSummaryCategoryTotal , error ) {
var results [] entities . ExclusiveSummaryCategoryTotal
query := r . db . WithContext ( ctx ).
Table ( "purchase_order_items poi" ).
Select ( `
pc.code as category_code,
pc.name as category_name,
COALESCE(SUM(poi.amount), 0) as amount
` ).
Joins ( "JOIN purchase_orders po ON poi.purchase_order_id = po.id" ).
Joins ( "JOIN purchase_categories pc ON poi.purchase_category_id = pc.id" ).
Joins ( "LEFT JOIN ingredients i ON poi.ingredient_id = i.id" ).
Joins ( "LEFT JOIN units u ON poi.unit_id = u.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "pc.type = ?" , entities . PurchaseCategoryTypeRawMaterial ).
Where ( "po.status = ?" , "received" ).
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo )
query = r . applyPurchaseOrderItemOutletFilter ( query , outletID )
err := query .
Group ( "pc.id, pc.code, pc.name, pc.sort_order" ).
Order ( "pc.sort_order ASC, pc.name ASC" ).
Scan ( & results ). Error
return results , err
}
2026-06-18 15:27:20 +07:00
func ( r * AnalyticsRepositoryImpl ) getExclusiveSummaryOperationalExpenseBreakdown ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . ExclusiveSummaryCategoryTotal , error ) {
2026-06-17 18:17:08 +07:00
var results [] entities . ExclusiveSummaryCategoryTotal
2026-06-18 15:27:20 +07:00
query := r . db . WithContext ( ctx ).
2026-06-17 18:17:08 +07:00
Table ( "purchase_order_items poi" ).
Select ( `
pc.code as category_code,
pc.name as category_name,
COALESCE(SUM(poi.amount), 0) as amount
` ).
Joins ( "JOIN purchase_orders po ON poi.purchase_order_id = po.id" ).
Joins ( "JOIN purchase_categories pc ON poi.purchase_category_id = pc.id" ).
Where ( "po.organization_id = ?" , organizationID ).
Where ( "pc.type = ?" , entities . PurchaseCategoryTypeExpense ).
Where ( "po.status = ?" , "received" ).
2026-06-18 15:27:20 +07:00
Where ( "po.transaction_date >= ? AND po.transaction_date <= ?" , dateFrom , dateTo )
query = r . applyPurchaseOrderItemOutletFilter ( query , outletID )
err := query .
2026-06-17 18:17:08 +07:00
Group ( "pc.id, pc.code, pc.name, pc.sort_order" ).
Order ( "pc.sort_order ASC, pc.name ASC" ).
Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) getExclusiveSummaryDailySummary ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . ExclusiveSummaryDailySummary , error ) {
var results [] entities . ExclusiveSummaryDailySummary
rawQuery , args := r . exclusiveSummaryPurchaseOrderItemQuery ( organizationID , outletID , dateFrom , dateTo )
err := r . db . WithContext ( ctx ). Raw ( `
SELECT date, COUNT(*) as transaction_count, COALESCE(SUM(amount), 0) as total_cost
FROM (` + rawQuery + `) transactions
GROUP BY date
ORDER BY date ASC
` , args ... ). Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) getExclusiveSummaryDailyTransactions ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ([] entities . ExclusiveSummaryDailyTransaction , error ) {
var results [] entities . ExclusiveSummaryDailyTransaction
rawQuery , args := r . exclusiveSummaryPurchaseOrderItemQuery ( organizationID , outletID , dateFrom , dateTo )
err := r . db . WithContext ( ctx ). Raw ( `
SELECT date, category_code, category_name, description, amount, source
FROM (` + rawQuery + `) transactions
ORDER BY date ASC, category_name ASC, description ASC
` , args ... ). Scan ( & results ). Error
return results , err
}
func ( r * AnalyticsRepositoryImpl ) exclusiveSummaryPurchaseOrderItemQuery ( organizationID uuid . UUID , outletID * uuid . UUID , dateFrom , dateTo time . Time ) ( string , [] interface {}) {
outletFilter := ""
args := [] interface {}{
organizationID ,
"received" ,
dateFrom ,
dateTo ,
}
if outletID != nil {
2026-06-18 15:27:20 +07:00
outletFilter = "AND po.outlet_id = ?"
args = append ( args , * outletID )
2026-06-17 18:17:08 +07:00
}
query := `
SELECT
DATE(po.transaction_date) as date,
pc.code as category_code,
pc.name as category_name,
COALESCE(NULLIF(poi.description, ''), i.name, pc.name) as description,
poi.amount as amount,
'purchase_order' as source
FROM purchase_order_items poi
JOIN purchase_orders po ON poi.purchase_order_id = po.id
JOIN purchase_categories pc ON poi.purchase_category_id = pc.id
LEFT JOIN ingredients i ON poi.ingredient_id = i.id
LEFT JOIN units u ON poi.unit_id = u.id
WHERE po.organization_id = ?
AND po.status = ?
AND po.transaction_date >= ? AND po.transaction_date <= ?
` + outletFilter + `
`
return query , args
}
2026-06-17 18:26:40 +07:00
func ( r * AnalyticsRepositoryImpl ) GetExclusiveSummaryBankBalances ( ctx context . Context , organizationID uuid . UUID , outletID * uuid . UUID ) ([] entities . ExclusiveSummaryBankBalance , error ) {
type accountBalance struct {
Name string
OpeningBalance float64
CurrentBalance float64
Description * string
}
var accounts [] accountBalance
query := r . db . WithContext ( ctx ).
Table ( "accounts" ).
Select ( "name, opening_balance, current_balance, description" ).
Where ( "organization_id = ?" , organizationID ).
Where ( "account_type IN ?" , [] entities . AccountType { entities . AccountTypeCash , entities . AccountTypeWallet , entities . AccountTypeBank }).
Where ( "is_active = ?" , true )
if outletID != nil {
query = query . Where ( "outlet_id = ? OR outlet_id IS NULL" , * outletID )
} else {
query = query . Where ( "outlet_id IS NULL" )
}
err := query .
Order ( "number ASC, name ASC" ).
Scan ( & accounts ). Error
if err != nil {
return nil , err
}
balances := make ([] entities . ExclusiveSummaryBankBalance , len ( accounts ))
for i , account := range accounts {
openingBalance := account . OpeningBalance
closingBalance := account . CurrentBalance
balances [ i ] = entities . ExclusiveSummaryBankBalance {
Bank : account . Name ,
OpeningBalance : & openingBalance ,
ClosingBalance : & closingBalance ,
Notes : account . Description ,
}
}
return balances , nil
}