Products like fish are sold per weighing (4.2 ons, 5.6 ons), which the order line could not represent: quantity is INTEGER and prices are always computed as quantity * unit_price. Model one weighing as one order line. quantity stays INTEGER and keeps meaning "how many items"; the measured amount goes into a new nullable order_items.weight, and the line is priced weight * unit_price. Two weighings of the same product are two lines, never merged into one. Keeping quantity integral avoids float comparisons in void, refund and split bill, where accumulated rounding error would silently misbehave — "1.4 + 1.4 + 1.4" is not 4.2 in float64, which would leave a fully paid split-bill item marked unpaid. BillableQuantity() is now the single place that decides between weight and count; every price and cost calculation goes through it. Missing one would bill a 4.2 ons fish as a single ons — wrong money, no error. Two database constraints back the design: a weighed line always carries a positive weight, and its quantity is pinned to 1. The latter also makes void all-or-nothing for weighed lines, so the row-splitting branch can never produce a zero-weight remainder row. Also wires product.unit_id through the API, which was previously not settable at all, and corrects the misleading comment on the request's unit_price field — that value has never been used; price always comes from the database. Design notes and the audit of every price multiplication site are in docs/rfc-weight-based-products.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
304 lines
11 KiB
Go
304 lines
11 KiB
Go
package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// PaymentMethodAnalytics represents payment method analytics data
|
|
type PaymentMethodAnalytics struct {
|
|
PaymentMethodID uuid.UUID `json:"payment_method_id"`
|
|
PaymentMethodName string `json:"payment_method_name"`
|
|
PaymentMethodType string `json:"payment_method_type"`
|
|
TotalAmount float64 `json:"total_amount"`
|
|
OrderCount int64 `json:"order_count"`
|
|
PaymentCount int64 `json:"payment_count"`
|
|
}
|
|
|
|
// SalesAnalytics represents sales analytics data
|
|
type SalesAnalytics struct {
|
|
Date time.Time `json:"date"`
|
|
Sales float64 `json:"sales"`
|
|
Orders int64 `json:"orders"`
|
|
Items int64 `json:"items"`
|
|
Tax float64 `json:"tax"`
|
|
Discount float64 `json:"discount"`
|
|
NetSales float64 `json:"net_sales"`
|
|
}
|
|
|
|
// PurchaseTeamFilter narrows purchasing figures to a single team: a parent
|
|
// category, Pusat, or the purchases that carry no team at all. A nil filter
|
|
// leaves the figures spanning every team.
|
|
type PurchaseTeamFilter struct {
|
|
Scope string
|
|
CategoryID *uuid.UUID
|
|
}
|
|
|
|
// PurchasingAnalytics represents purchasing analytics data
|
|
type PurchasingAnalytics struct {
|
|
OutletName *string `json:"outlet_name,omitempty"`
|
|
Summary PurchasingSummary `json:"summary"`
|
|
Data []PurchasingAnalyticsData `json:"data"`
|
|
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
|
VendorData []PurchasingVendorData `json:"vendor_data"`
|
|
TeamData []PurchasingTeamData `json:"team_data"`
|
|
}
|
|
|
|
// PurchasingTeamData is one team's share of the purchases: a parent category,
|
|
// Pusat, or the purchases charged to no team at all. Scope and CategoryID are
|
|
// what the team filter takes back, so a row can be clicked straight through.
|
|
type PurchasingTeamData struct {
|
|
Scope string `json:"scope"`
|
|
CategoryID *uuid.UUID `json:"category_id"`
|
|
Name string `json:"name"`
|
|
TotalPurchases float64 `json:"total_purchases"`
|
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
ExpensePurchases float64 `json:"expense_purchases"`
|
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
|
Quantity float64 `json:"quantity"`
|
|
Percentage float64 `json:"percentage"`
|
|
}
|
|
|
|
type PurchasingSummary struct {
|
|
TotalPurchases float64 `json:"total_purchases"`
|
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
ExpensePurchases float64 `json:"expense_purchases"`
|
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
|
ExpenseCount int64 `json:"expense_count"`
|
|
TotalQuantity float64 `json:"total_quantity"`
|
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
|
TotalIngredients int64 `json:"total_ingredients"`
|
|
TotalVendors int64 `json:"total_vendors"`
|
|
TotalTeams int64 `json:"total_teams"`
|
|
}
|
|
|
|
type PurchasingAnalyticsData struct {
|
|
Date time.Time `json:"date"`
|
|
Purchases float64 `json:"purchases"`
|
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
ExpensePurchases float64 `json:"expense_purchases"`
|
|
PurchaseOrders int64 `json:"purchase_orders"`
|
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
|
ExpenseCount int64 `json:"expense_count"`
|
|
Quantity float64 `json:"quantity"`
|
|
Ingredients int64 `json:"ingredients"`
|
|
Vendors int64 `json:"vendors"`
|
|
}
|
|
|
|
type PurchasingIngredientData struct {
|
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
|
IngredientName string `json:"ingredient_name"`
|
|
Quantity float64 `json:"quantity"`
|
|
TotalCost float64 `json:"total_cost"`
|
|
AverageUnitCost float64 `json:"average_unit_cost"`
|
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
|
}
|
|
|
|
type PurchasingVendorData struct {
|
|
VendorID *uuid.UUID `json:"vendor_id"`
|
|
VendorName string `json:"vendor_name"`
|
|
TotalCost float64 `json:"total_cost"`
|
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
|
IngredientCount int64 `json:"ingredient_count"`
|
|
Quantity float64 `json:"quantity"`
|
|
}
|
|
|
|
type ProductAnalytics struct {
|
|
ProductID uuid.UUID `json:"product_id"`
|
|
ProductName string `json:"product_name"`
|
|
ProductSku string `json:"product_sku"`
|
|
ProductPrice float64 `json:"product_price"`
|
|
CategoryID uuid.UUID `json:"category_id"`
|
|
CategoryName string `json:"category_name"`
|
|
CategoryOrder int `json:"category_order"`
|
|
QuantitySold int64 `json:"quantity_sold"`
|
|
WeightSold float64 `json:"weight_sold"`
|
|
Revenue float64 `json:"revenue"`
|
|
AveragePrice float64 `json:"average_price"`
|
|
OrderCount int64 `json:"order_count"`
|
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
|
}
|
|
|
|
type ProductAnalyticsPerCategory struct {
|
|
CategoryID uuid.UUID `json:"category_id"`
|
|
CategoryName string `json:"category_name"`
|
|
TotalRevenue float64 `json:"total_revenue"`
|
|
TotalQuantity int64 `json:"total_quantity"`
|
|
ProductCount int64 `json:"product_count"`
|
|
OrderCount int64 `json:"order_count"`
|
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
|
}
|
|
|
|
// ProductAnalyticsPerParentCategory rolls the per-category figures up to the
|
|
// top-level category. A category without a parent is its own group.
|
|
type ProductAnalyticsPerParentCategory struct {
|
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
|
ParentCategoryName string `json:"parent_category_name"`
|
|
TotalRevenue float64 `json:"total_revenue"`
|
|
TotalQuantity int64 `json:"total_quantity"`
|
|
CategoryCount int64 `json:"category_count"`
|
|
ProductCount int64 `json:"product_count"`
|
|
OrderCount int64 `json:"order_count"`
|
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
|
}
|
|
|
|
// ParentCategoryAnalyticsDetail is the drill-down for a single parent category:
|
|
// its own totals, the sub-categories underneath it, and the products in each.
|
|
type ParentCategoryAnalyticsDetail struct {
|
|
ParentCategoryID uuid.UUID
|
|
ParentCategoryName string
|
|
Summary *ProductAnalyticsPerParentCategory
|
|
Categories []*ProductAnalyticsPerCategory
|
|
Products []*ProductAnalytics
|
|
}
|
|
|
|
// BudgetCutOffWeek is one Monday-to-Sunday bucket of revenue, used to derive the
|
|
// weekly spending limits.
|
|
type BudgetCutOffWeek struct {
|
|
WeekStart time.Time `json:"week_start"`
|
|
Revenue float64 `json:"revenue"`
|
|
OrderCount int64 `json:"order_count"`
|
|
}
|
|
|
|
// DashboardOverview represents dashboard overview data
|
|
type DashboardOverview struct {
|
|
TotalSales float64 `json:"total_sales"`
|
|
TotalOrders int64 `json:"total_orders"`
|
|
AverageOrderValue float64 `json:"average_order_value"`
|
|
TotalCustomers int64 `json:"total_customers"`
|
|
VoidedOrders int64 `json:"voided_orders"`
|
|
RefundedOrders int64 `json:"refunded_orders"`
|
|
TotalItemSold int64 `json:"total_item_sold"`
|
|
TotalLowStock int64 `json:"total_low_stock"`
|
|
TotalProductActive int64 `json:"total_product_active"`
|
|
}
|
|
|
|
type ProfitLossAnalytics struct {
|
|
Summary ProfitLossSummary
|
|
Data []ProfitLossData
|
|
ProductData []ProductProfitData
|
|
TodayRevenue float64
|
|
TodayCost float64
|
|
MtdRevenue float64
|
|
MtdCost float64
|
|
TodayPurchasing float64
|
|
MtdPurchasing float64
|
|
TodayPurchasingRawMaterial float64
|
|
MtdPurchasingRawMaterial float64
|
|
TodayPurchasingExpense float64
|
|
MtdPurchasingExpense float64
|
|
PurchasingItems []PurchasingItemDetail
|
|
TodayExpenseByCategory []ExpenseCategoryTotal
|
|
MtdExpenseByCategory []ExpenseCategoryTotal
|
|
OperationalExpenseItems []OperationalExpenseItem
|
|
}
|
|
|
|
type PurchasingItemDetail struct {
|
|
Date time.Time
|
|
Item string
|
|
Quantity float64
|
|
Amount float64
|
|
}
|
|
|
|
type ProfitLossSummary struct {
|
|
TotalRevenue float64
|
|
TotalCost float64
|
|
GrossProfit float64
|
|
GrossProfitMargin float64
|
|
TotalTax float64
|
|
TotalDiscount float64
|
|
NetProfit float64
|
|
NetProfitMargin float64
|
|
TotalOrders int64
|
|
AverageProfit float64
|
|
ProfitabilityRatio float64
|
|
}
|
|
|
|
type ProfitLossData struct {
|
|
Date time.Time
|
|
Revenue float64
|
|
Cost float64
|
|
GrossProfit float64
|
|
GrossProfitMargin float64
|
|
Tax float64
|
|
Discount float64
|
|
NetProfit float64
|
|
NetProfitMargin float64
|
|
Orders int64
|
|
}
|
|
|
|
type ProductProfitData struct {
|
|
ProductID uuid.UUID
|
|
ProductName string
|
|
CategoryID uuid.UUID
|
|
CategoryName string
|
|
QuantitySold int64
|
|
Revenue float64
|
|
Cost float64
|
|
GrossProfit float64
|
|
GrossProfitMargin float64
|
|
AveragePrice float64
|
|
AverageCost float64
|
|
ProfitPerUnit float64
|
|
}
|
|
|
|
type ExpenseCategoryTotal struct {
|
|
CategoryName string
|
|
Amount float64
|
|
}
|
|
|
|
type OperationalExpenseItem struct {
|
|
Item string
|
|
Amount float64
|
|
}
|
|
|
|
type ExclusiveSummaryAnalytics struct {
|
|
SalesTotal float64
|
|
SalesCount int64
|
|
HPPBreakdown []ExclusiveSummaryCategoryTotal
|
|
OperationalExpenseBreakdown []ExclusiveSummaryCategoryTotal
|
|
DailySummary []ExclusiveSummaryDailySummary
|
|
DailyTransactions []ExclusiveSummaryDailyTransaction
|
|
}
|
|
|
|
type ExclusiveSummaryCategoryTotal struct {
|
|
CategoryCode string
|
|
CategoryName string
|
|
Amount float64
|
|
}
|
|
|
|
type ExclusiveSummaryDailySummary struct {
|
|
Date time.Time
|
|
TransactionCount int64
|
|
TotalCost float64
|
|
}
|
|
|
|
type ExclusiveSummaryDailyTransaction struct {
|
|
Date time.Time
|
|
CategoryCode string
|
|
CategoryName string
|
|
Description string
|
|
Amount float64
|
|
Source string
|
|
}
|
|
|
|
type ExclusiveSummaryBankBalance struct {
|
|
Bank string
|
|
OpeningBalance *float64
|
|
IncomingMutation *float64
|
|
OutgoingMutation *float64
|
|
ClosingBalance *float64
|
|
Notes *string
|
|
}
|