This commit is contained in:
2026-04-24 17:56:38 +07:00
parent 9b606b4c8b
commit ba970229a9
15 changed files with 206 additions and 1065 deletions
+56 -3
View File
@@ -124,11 +124,45 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity)
ELSE 0
END as average_price,
COUNT(DISTINCT oi.order_id) as order_count
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
`).
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").
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"),
).
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
@@ -141,7 +175,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
}
err := query.
Group("p.id, p.name, c.id, c.name, c.order").
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
Order("revenue DESC").
Limit(limit).
Scan(&results).Error
@@ -160,11 +194,30 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
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,
COUNT(DISTINCT p.id) as product_count,
COUNT(DISTINCT oi.order_id) as order_count
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
`).
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").
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"),
).
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
-227
View File
@@ -1,227 +0,0 @@
package repository
import (
"context"
"time"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type HPPRepository interface {
GetStandardHPP(ctx context.Context, organizationID uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID) ([]*entities.StandardHPPProduct, error)
GetStandardHPPIngredients(ctx context.Context, organizationID uuid.UUID, productIDs []uuid.UUID) ([]*entities.StandardHPPIngredient, error)
GetRealHPP(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.RealHPPProduct, error)
GetRealHPPTimeSeries(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.RealHPPTimeSeries, error)
}
type HPPRepositoryImpl struct {
db *gorm.DB
}
func NewHPPRepositoryImpl(db *gorm.DB) *HPPRepositoryImpl {
return &HPPRepositoryImpl{db: db}
}
func (r *HPPRepositoryImpl) GetStandardHPP(ctx context.Context, organizationID uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID) ([]*entities.StandardHPPProduct, error) {
var results []*entities.StandardHPPProduct
query := r.db.WithContext(ctx).
Table("products p").
Select(`
p.id as product_id,
p.name as product_name,
COALESCE(p.sku, '') as product_sku,
c.id as category_id,
c.name as category_name,
p.price as selling_price,
p.cost as product_cost,
COALESCE(SUM(
pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0)
), 0) as standard_cost,
CASE
WHEN p.price > 0
THEN COALESCE(SUM(pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0)), 0) / p.price * 100
ELSE 0
END as standard_hpp_percentage,
CASE WHEN COUNT(pr.id) > 0 THEN true ELSE false END as has_recipe
`).
Joins("LEFT JOIN product_recipes pr ON pr.product_id = p.id AND pr.organization_id = ?", organizationID).
Joins("LEFT JOIN ingredients i ON pr.ingredient_id = i.id").
Joins("JOIN categories c ON p.category_id = c.id").
Where("p.organization_id = ?", organizationID).
Where("p.is_active = ?", true)
if productID != nil {
query = query.Where("p.id = ?", *productID)
}
if categoryID != nil {
query = query.Where("p.category_id = ?", *categoryID)
}
err := query.
Group("p.id, p.name, p.sku, c.id, c.name, p.price, p.cost").
Order("p.name ASC").
Scan(&results).Error
return results, err
}
func (r *HPPRepositoryImpl) GetStandardHPPIngredients(ctx context.Context, organizationID uuid.UUID, productIDs []uuid.UUID) ([]*entities.StandardHPPIngredient, error) {
if len(productIDs) == 0 {
return []*entities.StandardHPPIngredient{}, nil
}
var results []*entities.StandardHPPIngredient
err := r.db.WithContext(ctx).
Table("product_recipes pr").
Select(`
pr.product_id,
pr.ingredient_id,
i.name as ingredient_name,
pr.quantity,
COALESCE(u.name, '') as unit_name,
i.cost as cost_per_unit,
COALESCE(pr.waste_percentage, 0) as waste_percentage,
pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0) as total_cost
`).
Joins("JOIN ingredients i ON pr.ingredient_id = i.id").
Joins("LEFT JOIN units u ON i.unit_id = u.id").
Where("pr.organization_id = ?", organizationID).
Where("pr.product_id IN ?", productIDs).
Order("pr.product_id, i.name").
Scan(&results).Error
return results, err
}
func (r *HPPRepositoryImpl) GetRealHPP(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.RealHPPProduct, error) {
var results []*entities.RealHPPProduct
query := r.db.WithContext(ctx).
Table("products p").
Select(`
p.id as product_id,
p.name as product_name,
COALESCE(p.sku, '') as product_sku,
c.id as category_id,
c.name as category_name,
p.price as selling_price,
COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0))
ELSE 0 END
), 0) as real_total_cost,
COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.total_price - COALESCE(oi.refund_amount, 0)
ELSE 0 END
), 0) as real_total_revenue,
CASE
WHEN COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.total_price - COALESCE(oi.refund_amount, 0)
ELSE 0 END
), 0) > 0
THEN COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0))
ELSE 0 END
), 0) / COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.total_price - COALESCE(oi.refund_amount, 0)
ELSE 0 END
), 0) * 100
ELSE 0
END as real_hpp_percentage,
COALESCE(SUM(
CASE WHEN oi.is_fully_refunded = false
THEN oi.quantity - COALESCE(oi.refund_quantity, 0)
ELSE 0 END
), 0) as total_quantity_sold,
COALESCE(COUNT(DISTINCT oi.order_id), 0) as total_orders
`).
Joins("JOIN categories c ON p.category_id = c.id").
Joins("LEFT JOIN order_items oi ON oi.product_id = p.id AND oi.status != 'cancelled'").
Joins("LEFT JOIN orders o ON oi.order_id = o.id AND o.is_void = false AND o.is_refund = false AND o.status = 'completed' AND o.payment_status = 'completed' AND o.organization_id = ? AND o.created_at >= ? AND o.created_at <= ?", organizationID, dateFrom, dateTo).
Where("p.organization_id = ?", organizationID).
Where("p.is_active = ?", true)
if outletID != nil {
query = query.Where("o.outlet_id = ? OR o.id IS NULL", *outletID)
}
if productID != nil {
query = query.Where("p.id = ?", *productID)
}
if categoryID != nil {
query = query.Where("p.category_id = ?", *categoryID)
}
err := query.
Group("p.id, p.name, p.sku, c.id, c.name, p.price").
Order("p.name ASC").
Scan(&results).Error
return results, err
}
func (r *HPPRepositoryImpl) GetRealHPPTimeSeries(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.RealHPPTimeSeries, error) {
var results []*entities.RealHPPTimeSeries
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)"
}
query := r.db.WithContext(ctx).
Table("orders o").
Select(`
`+timeFormat+` as date,
COALESCE(SUM(
oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0))
), 0) as real_total_cost,
COALESCE(SUM(
oi.total_price - COALESCE(oi.refund_amount, 0)
), 0) as real_total_revenue,
CASE
WHEN COALESCE(SUM(oi.total_price - COALESCE(oi.refund_amount, 0)), 0) > 0
THEN COALESCE(SUM(oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0))), 0) / COALESCE(SUM(oi.total_price - COALESCE(oi.refund_amount, 0)), 0) * 100
ELSE 0
END as real_hpp_percentage,
COUNT(DISTINCT o.id) as total_orders
`).
Joins("JOIN order_items oi ON oi.order_id = o.id AND oi.status != 'cancelled' AND oi.is_fully_refunded = false").
Where("o.organization_id = ?", organizationID).
Where("o.is_void = false").
Where("o.is_refund = false").
Where("o.status = 'completed'").
Where("o.payment_status = 'completed'").
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
if outletID != nil {
query = query.Where("o.outlet_id = ?", *outletID)
}
if productID != nil {
query = query.Where("oi.product_id = ?", *productID)
}
if categoryID != nil {
query = query.Where("oi.product_id IN (SELECT id FROM products WHERE category_id = ?)", *categoryID)
}
err := query.
Group(timeFormat).
Order(timeFormat).
Scan(&results).Error
return results, err
}