Files
TakeoutSaaS.TenantApi/src/Application/TakeoutSaaS.Application/App/Finance/Cost/Handlers/FinanceCostMapping.cs

249 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 成本模块映射与文案转换。
/// </summary>
internal static class FinanceCostMapping
{
/// <summary>
/// 维度编码转枚举。
/// </summary>
public static FinanceCostDimension ParseDimensionCode(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"store" => FinanceCostDimension.Store,
_ => FinanceCostDimension.Tenant
};
}
/// <summary>
/// 维度枚举转编码。
/// </summary>
public static string ToDimensionCode(FinanceCostDimension value)
{
return value == FinanceCostDimension.Store ? "store" : "tenant";
}
/// <summary>
/// 分类编码转枚举。
/// </summary>
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
};
}
/// <summary>
/// 分类枚举转编码。
/// </summary>
public static string ToCategoryCode(FinanceCostCategory value)
{
return value switch
{
FinanceCostCategory.FoodMaterial => "food",
FinanceCostCategory.Labor => "labor",
FinanceCostCategory.FixedExpense => "fixed",
FinanceCostCategory.PackagingConsumable => "packaging",
_ => "food"
};
}
/// <summary>
/// 分类文案。
/// </summary>
public static string ToCategoryText(FinanceCostCategory value)
{
return value switch
{
FinanceCostCategory.FoodMaterial => "食材原料",
FinanceCostCategory.Labor => "人工成本",
FinanceCostCategory.FixedExpense => "固定费用",
FinanceCostCategory.PackagingConsumable => "包装耗材",
_ => "食材原料"
};
}
/// <summary>
/// 格式化月份字符串yyyy-MM
/// </summary>
public static string ToMonthText(DateTime month)
{
return month.ToString("yyyy-MM", CultureInfo.InvariantCulture);
}
/// <summary>
/// 归一化金额精度。
/// </summary>
public static decimal RoundAmount(decimal value)
{
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 构建录入页 DTO。
/// </summary>
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
};
}
/// <summary>
/// 构建分析页 DTO。
/// </summary>
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
};
}
/// <summary>
/// 归一化为月份起始 UTC 时间。
/// </summary>
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);
}
}