using System.Globalization; using TakeoutSaaS.Application.App.Finance.Cost.Dto; using TakeoutSaaS.Domain.Finance.Enums; using TakeoutSaaS.Domain.Finance.Models; namespace TakeoutSaaS.Application.App.Finance.Cost.Handlers; /// /// 成本模块映射与文案转换。 /// internal static class FinanceCostMapping { /// /// 维度编码转枚举。 /// public static FinanceCostDimension ParseDimensionCode(string? value) { return (value ?? string.Empty).Trim().ToLowerInvariant() switch { "store" => FinanceCostDimension.Store, _ => FinanceCostDimension.Tenant }; } /// /// 维度枚举转编码。 /// public static string ToDimensionCode(FinanceCostDimension value) { return value == FinanceCostDimension.Store ? "store" : "tenant"; } /// /// 分类编码转枚举。 /// public static FinanceCostCategory ParseCategoryCode(string? value) { return (value ?? string.Empty).Trim().ToLowerInvariant() switch { "food" => FinanceCostCategory.FoodMaterial, "labor" => FinanceCostCategory.Labor, "fixed" => FinanceCostCategory.FixedExpense, "packaging" => FinanceCostCategory.PackagingConsumable, _ => FinanceCostCategory.FoodMaterial }; } /// /// 分类枚举转编码。 /// public static string ToCategoryCode(FinanceCostCategory value) { return value switch { FinanceCostCategory.FoodMaterial => "food", FinanceCostCategory.Labor => "labor", FinanceCostCategory.FixedExpense => "fixed", FinanceCostCategory.PackagingConsumable => "packaging", _ => "food" }; } /// /// 分类文案。 /// public static string ToCategoryText(FinanceCostCategory value) { return value switch { FinanceCostCategory.FoodMaterial => "食材原料", FinanceCostCategory.Labor => "人工成本", FinanceCostCategory.FixedExpense => "固定费用", FinanceCostCategory.PackagingConsumable => "包装耗材", _ => "食材原料" }; } /// /// 格式化月份字符串(yyyy-MM)。 /// public static string ToMonthText(DateTime month) { return month.ToString("yyyy-MM", CultureInfo.InvariantCulture); } /// /// 归一化金额精度。 /// public static decimal RoundAmount(decimal value) { return decimal.Round(value, 2, MidpointRounding.AwayFromZero); } /// /// 构建录入页 DTO。 /// public static FinanceCostEntryDto ToEntryDto(FinanceCostMonthSnapshot snapshot) { // 1. 计算总成本与成本率。 var totalCost = RoundAmount(snapshot.Categories.Sum(item => item.TotalAmount)); var costRate = snapshot.MonthRevenue > 0 ? RoundAmount(totalCost / snapshot.MonthRevenue * 100m) : 0m; // 2. 映射分类与明细。 var categories = snapshot.Categories.Select(category => { var percentage = totalCost > 0 ? RoundAmount(category.TotalAmount / totalCost * 100m) : 0m; return new FinanceCostEntryCategoryDto { Category = ToCategoryCode(category.Category), CategoryText = ToCategoryText(category.Category), TotalAmount = RoundAmount(category.TotalAmount), Percentage = percentage, Items = category.Items .OrderBy(item => item.SortOrder) .ThenBy(item => item.ItemName) .Select(item => new FinanceCostEntryDetailDto { ItemId = item.ItemId?.ToString(CultureInfo.InvariantCulture), ItemName = item.ItemName, Amount = RoundAmount(item.Amount), Quantity = item.Quantity.HasValue ? RoundAmount(item.Quantity.Value) : null, UnitPrice = item.UnitPrice.HasValue ? RoundAmount(item.UnitPrice.Value) : null, SortOrder = item.SortOrder }) .ToList() }; }).ToList(); return new FinanceCostEntryDto { Dimension = ToDimensionCode(snapshot.Dimension), StoreId = snapshot.StoreId?.ToString(CultureInfo.InvariantCulture), Month = ToMonthText(snapshot.CostMonth), MonthRevenue = RoundAmount(snapshot.MonthRevenue), TotalCost = totalCost, CostRate = costRate, Categories = categories }; } /// /// 构建分析页 DTO。 /// public static FinanceCostAnalysisDto ToAnalysisDto(FinanceCostAnalysisSnapshot snapshot) { // 1. 计算统计指标。 var averageCostPerPaidOrder = snapshot.CurrentPaidOrderCount > 0 ? RoundAmount(snapshot.CurrentTotalCost / snapshot.CurrentPaidOrderCount) : 0m; var foodCostRate = snapshot.CurrentRevenue > 0 ? RoundAmount(snapshot.CurrentFoodAmount / snapshot.CurrentRevenue * 100m) : 0m; // 2. 映射趋势与明细表。 var trend = snapshot.Trends .OrderBy(item => item.MonthStartUtc) .Select(item => { var costRate = item.Revenue > 0 ? RoundAmount(item.TotalCost / item.Revenue * 100m) : 0m; return new FinanceCostTrendPointDto { Month = ToMonthText(item.MonthStartUtc), TotalCost = RoundAmount(item.TotalCost), Revenue = RoundAmount(item.Revenue), CostRate = costRate }; }) .ToList(); var detailRows = snapshot.DetailRows .OrderByDescending(item => item.MonthStartUtc) .Select(item => { var costRate = item.Revenue > 0 ? RoundAmount(item.TotalCost / item.Revenue * 100m) : 0m; return new FinanceCostMonthlyDetailRowDto { Month = ToMonthText(item.MonthStartUtc), FoodAmount = RoundAmount(item.FoodAmount), LaborAmount = RoundAmount(item.LaborAmount), FixedAmount = RoundAmount(item.FixedAmount), PackagingAmount = RoundAmount(item.PackagingAmount), TotalCost = RoundAmount(item.TotalCost), CostRate = costRate }; }) .ToList(); // 3. 构建成本构成。 var totalCost = RoundAmount(snapshot.CurrentTotalCost); var composition = snapshot.CurrentCategories .OrderBy(item => item.Category) .Select(item => new FinanceCostCompositionItemDto { Category = ToCategoryCode(item.Category), CategoryText = ToCategoryText(item.Category), Amount = RoundAmount(item.TotalAmount), Percentage = totalCost > 0 ? RoundAmount(item.TotalAmount / totalCost * 100m) : 0m }) .ToList(); return new FinanceCostAnalysisDto { Dimension = ToDimensionCode(snapshot.Dimension), StoreId = snapshot.StoreId?.ToString(CultureInfo.InvariantCulture), Month = ToMonthText(snapshot.CostMonth), Stats = new FinanceCostAnalysisStatsDto { TotalCost = totalCost, FoodCostRate = foodCostRate, AverageCostPerPaidOrder = averageCostPerPaidOrder, MonthOnMonthChangeRate = RoundAmount(snapshot.MonthOnMonthChangeRate), Revenue = RoundAmount(snapshot.CurrentRevenue), PaidOrderCount = snapshot.CurrentPaidOrderCount }, Trend = trend, Composition = composition, DetailRows = detailRows }; } /// /// 归一化为月份起始 UTC 时间。 /// public static DateTime NormalizeMonthStart(DateTime value) { var utcValue = value.Kind switch { DateTimeKind.Utc => value, DateTimeKind.Local => value.ToUniversalTime(), _ => DateTime.SpecifyKind(value, DateTimeKind.Utc) }; return new DateTime(utcValue.Year, utcValue.Month, 1, 0, 0, 0, DateTimeKind.Utc); } }