apskel-pos-backend/internal/transformer/analytics_transformer.go
2026-06-10 13:18:49 +07:00

562 lines
18 KiB
Go

package transformer
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/util"
"fmt"
"time"
"github.com/google/uuid"
)
// parseOutletID converts a *string outlet ID to *uuid.UUID, returning nil for invalid/empty values.
func parseOutletID(s *string) *uuid.UUID {
if s == nil {
return nil
}
id, err := uuid.Parse(*s)
if err != nil {
return nil
}
return &id
}
// PaymentMethodAnalyticsContractToModel converts contract request to model
func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest {
var dateFrom, dateTo time.Time
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.PaymentMethodAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
GroupBy: req.GroupBy,
}
}
// PaymentMethodAnalyticsModelToContract converts model response to contract
func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsResponse) *contract.PaymentMethodAnalyticsResponse {
if resp == nil {
return nil
}
var data []contract.PaymentMethodAnalyticsData
for _, item := range resp.Data {
data = append(data, contract.PaymentMethodAnalyticsData{
PaymentMethodID: item.PaymentMethodID,
PaymentMethodName: item.PaymentMethodName,
PaymentMethodType: item.PaymentMethodType,
TotalAmount: item.TotalAmount,
OrderCount: item.OrderCount,
PaymentCount: item.PaymentCount,
Percentage: item.Percentage,
})
}
return &contract.PaymentMethodAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
GroupBy: resp.GroupBy,
Summary: contract.PaymentMethodSummary{
TotalAmount: resp.Summary.TotalAmount,
TotalOrders: resp.Summary.TotalOrders,
TotalPayments: resp.Summary.TotalPayments,
AverageOrderValue: resp.Summary.AverageOrderValue,
},
Data: data,
}
}
func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.SalesAnalyticsRequest {
var dateFrom, dateTo time.Time
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.SalesAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
GroupBy: req.GroupBy,
}
}
// SalesAnalyticsModelToContract converts model response to contract
func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contract.SalesAnalyticsResponse {
if resp == nil {
return nil
}
var data []contract.SalesAnalyticsData
for _, item := range resp.Data {
data = append(data, contract.SalesAnalyticsData{
Date: item.Date,
Sales: item.Sales,
Orders: item.Orders,
Items: item.Items,
Tax: item.Tax,
Discount: item.Discount,
NetSales: item.NetSales,
})
}
return &contract.SalesAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
GroupBy: resp.GroupBy,
Summary: contract.SalesSummary{
TotalSales: resp.Summary.TotalSales,
TotalOrders: resp.Summary.TotalOrders,
TotalItems: resp.Summary.TotalItems,
AverageOrderValue: resp.Summary.AverageOrderValue,
TotalTax: resp.Summary.TotalTax,
TotalDiscount: resp.Summary.TotalDiscount,
NetSales: resp.Summary.NetSales,
},
Data: data,
}
}
// PurchasingAnalyticsContractToModel converts contract request to model
func PurchasingAnalyticsContractToModel(req *contract.PurchasingAnalyticsRequest) *models.PurchasingAnalyticsRequest {
var dateFrom, dateTo time.Time
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.PurchasingAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
GroupBy: req.GroupBy,
}
}
// PurchasingAnalyticsModelToContract converts model response to contract
func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse) *contract.PurchasingAnalyticsResponse {
if resp == nil {
return nil
}
data := make([]contract.PurchasingAnalyticsData, len(resp.Data))
for i, item := range resp.Data {
data[i] = contract.PurchasingAnalyticsData{
Date: item.Date,
Purchases: item.Purchases,
RawMaterialPurchases: item.RawMaterialPurchases,
NonInventoryPurchases: item.NonInventoryPurchases,
PurchaseOrders: item.PurchaseOrders,
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
Quantity: item.Quantity,
Ingredients: item.Ingredients,
Vendors: item.Vendors,
}
}
ingredientData := make([]contract.PurchasingIngredientData, len(resp.IngredientData))
for i, item := range resp.IngredientData {
ingredientData[i] = contract.PurchasingIngredientData{
IngredientID: item.IngredientID,
IngredientName: item.IngredientName,
Quantity: item.Quantity,
TotalCost: item.TotalCost,
AverageUnitCost: item.AverageUnitCost,
PurchaseOrderCount: item.PurchaseOrderCount,
}
}
vendorData := make([]contract.PurchasingVendorData, len(resp.VendorData))
for i, item := range resp.VendorData {
vendorData[i] = contract.PurchasingVendorData{
VendorID: item.VendorID,
VendorName: item.VendorName,
TotalCost: item.TotalCost,
PurchaseOrderCount: item.PurchaseOrderCount,
IngredientCount: item.IngredientCount,
Quantity: item.Quantity,
}
}
return &contract.PurchasingAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
GroupBy: resp.GroupBy,
Summary: contract.PurchasingSummary{
TotalPurchases: resp.Summary.TotalPurchases,
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
NonInventoryPurchases: resp.Summary.NonInventoryPurchases,
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
NonInventoryExpenseCount: resp.Summary.NonInventoryExpenseCount,
TotalQuantity: resp.Summary.TotalQuantity,
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
TotalIngredients: resp.Summary.TotalIngredients,
TotalVendors: resp.Summary.TotalVendors,
},
Data: data,
IngredientData: ingredientData,
VendorData: vendorData,
}
}
// ProductAnalyticsContractToModel converts contract request to model
func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *models.ProductAnalyticsRequest {
var dateFrom, dateTo time.Time
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.ProductAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
Limit: req.Limit,
}
}
// ProductAnalyticsModelToContract converts model response to contract
func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *contract.ProductAnalyticsResponse {
if resp == nil {
return nil
}
var data []contract.ProductAnalyticsData
for _, item := range resp.Data {
data = append(data, contract.ProductAnalyticsData{
ProductID: item.ProductID,
ProductName: item.ProductName,
ProductSku: item.ProductSku,
ProductPrice: item.ProductPrice,
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
CategoryOrder: item.CategoryOrder,
QuantitySold: item.QuantitySold,
Revenue: item.Revenue,
AveragePrice: item.AveragePrice,
OrderCount: item.OrderCount,
StandardHppPerUnit: item.StandardHppPerUnit,
StandardHppTotal: item.StandardHppTotal,
FifoHppPerUnit: item.FifoHppPerUnit,
FifoHppTotal: item.FifoHppTotal,
MovingAverageHppPerUnit: item.MovingAverageHppPerUnit,
MovingAverageHppTotal: item.MovingAverageHppTotal,
})
}
return &contract.ProductAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
Data: data,
}
}
// ProductAnalyticsPerCategoryContractToModel converts contract request to model
func ProductAnalyticsPerCategoryContractToModel(req *contract.ProductAnalyticsPerCategoryRequest) *models.ProductAnalyticsPerCategoryRequest {
var dateFrom, dateTo time.Time
// Parse date range using utility function
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.ProductAnalyticsPerCategoryRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
}
}
// ProductAnalyticsPerCategoryModelToContract converts model response to contract
func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPerCategoryResponse) *contract.ProductAnalyticsPerCategoryResponse {
if resp == nil {
return nil
}
var data []contract.ProductAnalyticsPerCategoryData
for _, item := range resp.Data {
data = append(data, contract.ProductAnalyticsPerCategoryData{
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
TotalRevenue: item.TotalRevenue,
TotalQuantity: item.TotalQuantity,
ProductCount: item.ProductCount,
OrderCount: item.OrderCount,
TotalStandardHpp: item.TotalStandardHpp,
TotalFifoHpp: item.TotalFifoHpp,
TotalMovingAverageHpp: item.TotalMovingAverageHpp,
})
}
return &contract.ProductAnalyticsPerCategoryResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
Data: data,
}
}
// DashboardAnalyticsContractToModel converts contract request to model
func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest) *models.DashboardAnalyticsRequest {
var dateFrom, dateTo time.Time
// Parse date range using utility function
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil {
if fromTime != nil {
dateFrom = *fromTime
}
if toTime != nil {
dateTo = *toTime
}
}
return &models.DashboardAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: dateFrom,
DateTo: dateTo,
}
}
// DashboardAnalyticsModelToContract converts model response to contract
func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse) *contract.DashboardAnalyticsResponse {
if resp == nil {
return nil
}
var topProducts []contract.ProductAnalyticsData
for _, item := range resp.TopProducts {
topProducts = append(topProducts, contract.ProductAnalyticsData{
ProductID: item.ProductID,
ProductName: item.ProductName,
ProductPrice: item.ProductPrice,
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
QuantitySold: item.QuantitySold,
Revenue: item.Revenue,
AveragePrice: item.AveragePrice,
OrderCount: item.OrderCount,
StandardHppPerUnit: item.StandardHppPerUnit,
StandardHppTotal: item.StandardHppTotal,
FifoHppPerUnit: item.FifoHppPerUnit,
FifoHppTotal: item.FifoHppTotal,
MovingAverageHppPerUnit: item.MovingAverageHppPerUnit,
MovingAverageHppTotal: item.MovingAverageHppTotal,
})
}
var paymentMethods []contract.PaymentMethodAnalyticsData
for _, item := range resp.PaymentMethods {
paymentMethods = append(paymentMethods, contract.PaymentMethodAnalyticsData{
PaymentMethodID: item.PaymentMethodID,
PaymentMethodName: item.PaymentMethodName,
PaymentMethodType: item.PaymentMethodType,
TotalAmount: item.TotalAmount,
OrderCount: item.OrderCount,
PaymentCount: item.PaymentCount,
Percentage: item.Percentage,
})
}
var recentSales []contract.SalesAnalyticsData
for _, item := range resp.RecentSales {
recentSales = append(recentSales, contract.SalesAnalyticsData{
Date: item.Date,
Sales: item.Sales,
Orders: item.Orders,
Items: item.Items,
Tax: item.Tax,
Discount: item.Discount,
NetSales: item.NetSales,
})
}
return &contract.DashboardAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
Overview: contract.DashboardOverview{
TotalSales: resp.Overview.TotalSales,
TotalOrders: resp.Overview.TotalOrders,
AverageOrderValue: resp.Overview.AverageOrderValue,
TotalCustomers: resp.Overview.TotalCustomers,
VoidedOrders: resp.Overview.VoidedOrders,
RefundedOrders: resp.Overview.RefundedOrders,
},
TopProducts: topProducts,
PaymentMethods: paymentMethods,
RecentSales: recentSales,
}
}
func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsRequest, error) {
if req == nil {
return nil, fmt.Errorf("request cannot be nil")
}
dateFrom, dateTo, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo)
if err != nil {
return nil, fmt.Errorf("invalid date range: %w", err)
}
if dateFrom == nil {
return nil, fmt.Errorf("date_from is required")
}
if dateTo == nil {
return nil, fmt.Errorf("date_to is required")
}
return &models.ProfitLossAnalyticsRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateFrom: *dateFrom,
DateTo: *dateTo,
GroupBy: req.GroupBy,
}, nil
}
func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse) *contract.ProfitLossAnalyticsResponse {
if resp == nil {
return nil
}
mainSummary := make([]contract.ProfitLossSummaryRow, len(resp.MainSummary))
for i, row := range resp.MainSummary {
mainSummary[i] = profitLossSummaryRowModelToContract(row)
}
data := make([]contract.ProfitLossData, len(resp.Data))
for i, item := range resp.Data {
data[i] = contract.ProfitLossData{
Date: item.Date,
Revenue: item.Revenue,
Cost: item.Cost,
GrossProfit: item.GrossProfit,
GrossProfitMargin: item.GrossProfitMargin,
Tax: item.Tax,
Discount: item.Discount,
NetProfit: item.NetProfit,
NetProfitMargin: item.NetProfitMargin,
Orders: item.Orders,
}
}
productData := make([]contract.ProductProfitData, len(resp.ProductData))
for i, item := range resp.ProductData {
productData[i] = contract.ProductProfitData{
ProductID: item.ProductID,
ProductName: item.ProductName,
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
QuantitySold: item.QuantitySold,
Revenue: item.Revenue,
Cost: item.Cost,
GrossProfit: item.GrossProfit,
GrossProfitMargin: item.GrossProfitMargin,
AveragePrice: item.AveragePrice,
AverageCost: item.AverageCost,
ProfitPerUnit: item.ProfitPerUnit,
}
}
opsItems := make([]contract.OperationalExpenseItem, len(resp.OperationalExpenses))
for i, item := range resp.OperationalExpenses {
opsItems[i] = contract.OperationalExpenseItem{
Item: item.Item,
Nominal: item.Nominal,
}
}
return &contract.ProfitLossAnalyticsResponse{
OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID,
DateFrom: resp.DateFrom,
DateTo: resp.DateTo,
GroupBy: resp.GroupBy,
Summary: contract.ProfitLossSummary{
TotalRevenue: resp.Summary.TotalRevenue,
TotalCost: resp.Summary.TotalCost,
GrossProfit: resp.Summary.GrossProfit,
GrossProfitMargin: resp.Summary.GrossProfitMargin,
TotalTax: resp.Summary.TotalTax,
TotalDiscount: resp.Summary.TotalDiscount,
NetProfit: resp.Summary.NetProfit,
NetProfitMargin: resp.Summary.NetProfitMargin,
TotalOrders: resp.Summary.TotalOrders,
AverageProfit: resp.Summary.AverageProfit,
ProfitabilityRatio: resp.Summary.ProfitabilityRatio,
},
Data: data,
ProductData: productData,
MainSummary: mainSummary,
OperationalExpenses: opsItems,
OperationalExpensesTotal: resp.OperationalExpensesTotal,
}
}
func profitLossSummaryRowModelToContract(row models.ProfitLossSummaryRow) contract.ProfitLossSummaryRow {
subItems := make([]contract.ProfitLossSummaryRow, len(row.SubItems))
for i, sub := range row.SubItems {
subItems[i] = profitLossSummaryRowModelToContract(sub)
}
return contract.ProfitLossSummaryRow{
ID: row.ID,
Label: row.Label,
IsBold: row.IsBold,
TodayNominal: row.TodayNominal,
TodayPct: row.TodayPct,
MtdNominal: row.MtdNominal,
MtdPct: row.MtdPct,
SubItems: subItems,
}
}