feat: slim rules/price publish snapshots with PostcodeRange (left,right]

Use slim DTOs without audit timestamps; migrate legacy LeftPostcode/RightPostcode on read.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-06-26 14:10:22 +08:00
parent d8a98fbbc8
commit 7827b4803e
16 changed files with 519 additions and 150 deletions

View File

@ -23,14 +23,14 @@ public class SalesProductPricePublishSnapshotDto
public string? PriceAreaName { get; set; }
/// <summary>
/// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化。
/// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
/// </summary>
public List<KpiAreaInfoOutput> PriceAreaInfos { get; set; } = [];
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
/// <summary>
/// 价格项列表。
/// 价格项列表(不含创建/更新时间)。
/// </summary>
public List<SalesProductPriceListOutput> Prices { get; set; } = [];
public List<SalesProductPriceSnapshotItemDto> Prices { get; set; } = [];
}
/// <summary>
@ -59,9 +59,9 @@ public class SalesProductRulesPublishSnapshotDto
public string? BlacklistAreaName { get; set; }
/// <summary>
/// 黑名单分区明细,发布时从 t_kpi_area_info 固化。
/// 黑名单分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
/// </summary>
public List<KpiAreaInfoOutput> BlacklistAreaInfos { get; set; } = [];
public List<KpiAreaSnapshotRuleDto> BlacklistAreaInfos { get; set; } = [];
/// <summary>
/// 偏远分区 ID(发布时固化)。
@ -74,9 +74,9 @@ public class SalesProductRulesPublishSnapshotDto
public string? RemoteAreaName { get; set; }
/// <summary>
/// 偏远分区明细,发布时从 t_kpi_area_info 固化。
/// 偏远分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
/// </summary>
public List<KpiAreaInfoOutput> RemoteAreaInfos { get; set; } = [];
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
/// <summary>
/// 计重规则 ID(发布时固化)。
@ -91,7 +91,7 @@ public class SalesProductRulesPublishSnapshotDto
/// <summary>
/// 计重规则明细,发布时从 t_weight_rule_info 固化。
/// </summary>
public List<WeightRuleInfoOutput> WeightRuleInfos { get; set; } = [];
public List<WeightRuleSnapshotRuleDto> WeightRuleInfos { get; set; } = [];
/// <summary>
/// 计重进位 ID(发布时固化)。
@ -106,7 +106,7 @@ public class SalesProductRulesPublishSnapshotDto
/// <summary>
/// 计重进位区间,发布时从 t_weight_rounding_info 固化。
/// </summary>
public List<WeightRoundingInfoOutput> WeightRoundingInfos { get; set; } = [];
public List<WeightRoundingSnapshotRangeDto> WeightRoundingInfos { get; set; } = [];
}
/// <summary>

View File

@ -0,0 +1,86 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Oms.Application.Dtos.Channels;
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// KPI 分区快照行(黑名单/偏远/价格分区),仅保留计费匹配字段。
/// </summary>
public class KpiAreaSnapshotRuleDto
{
public int Id { get; set; }
public string? CountryCode { get; set; }
public string? Province { get; set; }
public string? City { get; set; }
/// <summary>
/// 邮编区间,左开右闭,如 (50000,90000]。
/// </summary>
public string? PostcodeRange { get; set; }
/// <summary>旧快照兼容:反序列化后由 <see cref="PublishSnapshotNormalizer"/> 转为 PostcodeRange 并清空。</summary>
public string? LeftPostcode { get; set; }
/// <summary>旧快照兼容:反序列化后由 <see cref="PublishSnapshotNormalizer"/> 转为 PostcodeRange 并清空。</summary>
public string? RightPostcode { get; set; }
}
/// <summary>
/// 计重规则快照行。
/// </summary>
public class WeightRuleSnapshotRuleDto
{
public int Id { get; set; }
public string Condition { get; set; } = string.Empty;
public string BillableWeight { get; set; } = string.Empty;
public string VolumetricFactor { get; set; } = string.Empty;
}
/// <summary>
/// 计重进位快照行。
/// </summary>
public class WeightRoundingSnapshotRangeDto
{
public int Id { get; set; }
public decimal Start { get; set; }
public decimal End { get; set; }
public string? Public { get; set; }
}
/// <summary>
/// 卖价项快照(不含创建/更新时间)。
/// </summary>
public class SalesProductPriceSnapshotItemDto
{
public int Id { get; set; }
public int SerialNumber { get; set; }
public byte Status { get; set; }
public string PriceName { get; set; } = string.Empty;
public string CostItem { get; set; } = string.Empty;
public string CostUnit { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
public string? ChargingTime { get; set; }
public string? ChargingCondition { get; set; }
public List<ChannelPriceFactorOutput> Factors { get; set; } = [];
public List<ChannelPriceDetailOutput> Details { get; set; } = [];
}

View File

@ -184,7 +184,7 @@ public partial class SalesProductService
var priceSnapshot = new SalesProductPricePublishSnapshot
{
SalesProductId = salesProductId,
Prices = await GetPriceOutputsAsync(salesProductId)
Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
};
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository);
return priceSnapshot;
@ -230,7 +230,7 @@ public partial class SalesProductService
PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId<int>().ToString() : CurrentUser.UserName!,
PublishTime = DateTime.Now,
Description = description,
Content = OmsDocHelper.SerializeJson(snapshot)
Content = OmsDocHelper.SerializeSnapshotJson(snapshot)
});
}
@ -249,7 +249,7 @@ public partial class SalesProductService
if (operatorType == PriceOperatorType)
{
var snapshot = OmsDocHelper.DeserializeJson<SalesProductPricePublishSnapshot>(record.Content);
var snapshot = OmsDocHelper.DeserializePriceSnapshot(record.Content);
if (snapshot == null)
{
throw new Fake.Application.BusinessException("发布快照无效");
@ -259,12 +259,12 @@ public partial class SalesProductService
await SavePriceAsync(new SalesProductPriceSaveAllInput
{
SalesProductId = input.SalesProductId,
Prices = snapshot.Prices.Select(MapPriceSaveInput).ToList()
Prices = snapshot.Prices.Select(price => MapPriceSaveInput(price, input.SalesProductId)).ToList()
});
return;
}
var rulesSnapshot = OmsDocHelper.DeserializeJson<SalesProductRulesPublishSnapshot>(record.Content);
var rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(record.Content);
if (rulesSnapshot == null)
{
throw new Fake.Application.BusinessException("发布快照无效");
@ -408,12 +408,47 @@ public partial class SalesProductService
}).ToList();
}
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output)
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output) =>
MapPriceSaveInput(output, output.SalesProductId);
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceSnapshotItemDto output, int salesProductId)
{
return new SalesProductPriceSaveInput
{
Id = output.Id,
SalesProductId = output.SalesProductId,
SalesProductId = salesProductId,
SerialNumber = output.SerialNumber,
Status = (EnableStatus)output.Status,
PriceName = output.PriceName,
CostItem = output.CostItem,
CostUnit = output.CostUnit,
Currency = output.Currency,
ChargingTime = output.ChargingTime,
ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{
Id = item.Id,
SerialNumber = item.SerialNumber,
Factor = item.Factor,
FactorValues = item.FactorValues
}).ToList(),
Details = output.Details.Select(item => new ChannelPriceDetailInput
{
Id = item.Id,
FactorValuesJson = item.FactorValuesJson,
CalculationFormula = item.CalculationFormula,
CalculatedValue = item.CalculatedValue,
CustomerGroupValues = item.CustomerGroupValues
}).ToList()
};
}
private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output, int salesProductId)
{
return new SalesProductPriceSaveInput
{
Id = output.Id,
SalesProductId = salesProductId,
SerialNumber = output.SerialNumber,
Status = (EnableStatus)output.Status,
PriceName = output.PriceName,

View File

@ -250,7 +250,7 @@ public partial class OrderService
}
else
{
rulesSnapshot = OmsDocHelper.DeserializeJson<SalesProductRulesPublishSnapshotDto>(rulesRecord.Content)
rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord.Content)
?? throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
}
@ -265,7 +265,7 @@ public partial class OrderService
}
else
{
priceSnapshot = OmsDocHelper.DeserializeJson<SalesProductPricePublishSnapshotDto>(priceRecord.Content)
priceSnapshot = OmsDocHelper.DeserializePriceSnapshot(priceRecord.Content)
?? throw new BusinessException("未命中价格快照版本(内存/Redis/DB)");
if (priceSnapshot.Prices.Count == 0)
{
@ -330,7 +330,7 @@ public partial class OrderService
var snapshot = new SalesProductPricePublishSnapshotDto
{
SalesProductId = product.Id,
Prices = prices.Select(price => new SalesProductPriceListOutput
Prices = prices.Select(price => PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
{
Id = price.Id,
SalesProductId = price.SalesProductId,
@ -348,7 +348,7 @@ public partial class OrderService
Details = detailMap.TryGetValue(price.Id, out var details)
? details.Select(MapPriceDetailOutput).ToList()
: []
}).ToList()
})).ToList()
};
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(snapshot, product, KpiAreaRepository);
return snapshot;

View File

@ -146,7 +146,7 @@ internal static class ParcelBillingCalculator
}
var matched = priceSnapshot.PriceAreaInfos.FirstOrDefault(rule =>
IsAreaMatched(rule, countryCode, province, city, zipCode));
KpiAreaSnapshotMapper.IsRuleMatched(rule, countryCode, province, city, zipCode));
if (matched == null)
{
return null;
@ -221,6 +221,62 @@ internal static class ParcelBillingCalculator
return keys.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
public static IReadOnlyList<string> BuildPriceZoneKeys(
KpiAreaSnapshotRuleDto matched,
string? areaName,
int? areaId)
{
var keys = new List<string>();
var name = ResolveZoneName(matched, areaName);
if (!string.IsNullOrWhiteSpace(name))
{
keys.Add(name);
}
keys.Add(matched.Id.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrWhiteSpace(matched.Province))
{
keys.Add(matched.Province);
}
if (!string.IsNullOrWhiteSpace(matched.CountryCode))
{
keys.Add(matched.CountryCode);
}
if (areaId is > 0)
{
keys.Add(areaId.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(areaName))
{
keys.Add(areaName);
}
return keys.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
public static string? ResolveZoneName(KpiAreaSnapshotRuleDto matched, string? areaName)
{
if (!string.IsNullOrWhiteSpace(matched.Province))
{
return matched.Province;
}
if (!string.IsNullOrWhiteSpace(matched.City))
{
return matched.City;
}
if (!string.IsNullOrWhiteSpace(matched.CountryCode))
{
return matched.CountryCode;
}
return areaName;
}
public static IReadOnlyList<string> BuildPriceZoneKeys(
KpiAreaInfoOutput matched,
string? areaName,

View File

@ -0,0 +1,105 @@
using System.Text.RegularExpressions;
using Oms.Application.Dtos.Orders;
using Oms.Domain.ChannelsAggregate.Entities;
namespace Oms.Application.Shared;
/// <summary>
/// KPI 分区快照映射与左开右闭邮编匹配。
/// </summary>
public static class KpiAreaSnapshotMapper
{
private static readonly Regex PostcodeRangePattern = new(
@"^\(\s*([^,)\]]*)\s*,\s*([^,)\]]*)\s*\]$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity) =>
new()
{
Id = entity.Id,
CountryCode = entity.CountryCode,
Province = entity.Province,
City = entity.City,
PostcodeRange = BuildPostcodeRange(entity.LeftPostcode, entity.RightPostcode)
};
public static string? BuildPostcodeRange(string? left, string? right)
{
if (string.IsNullOrWhiteSpace(left) && string.IsNullOrWhiteSpace(right))
{
return null;
}
return $"({left?.Trim()},{right?.Trim()}]";
}
public static string? ResolvePostcodeRange(KpiAreaSnapshotRuleDto rule)
{
if (!string.IsNullOrWhiteSpace(rule.PostcodeRange))
{
return rule.PostcodeRange.Trim();
}
return BuildPostcodeRange(rule.LeftPostcode, rule.RightPostcode);
}
public static bool IsRuleMatched(
KpiAreaSnapshotRuleDto rule,
string countryCode,
string? province,
string? city,
string? zipCode)
{
if (!string.IsNullOrWhiteSpace(rule.CountryCode)
&& !string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrWhiteSpace(rule.Province)
&& !string.Equals(rule.Province, province, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrWhiteSpace(rule.City)
&& !string.Equals(rule.City, city, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var postcodeRange = ResolvePostcodeRange(rule);
if (string.IsNullOrWhiteSpace(postcodeRange))
{
return true;
}
if (string.IsNullOrWhiteSpace(zipCode))
{
return false;
}
return IsZipInLeftOpenRightClosedRange(zipCode.Trim(), postcodeRange);
}
public static bool IsZipInLeftOpenRightClosedRange(string zipCode, string postcodeRange)
{
var match = PostcodeRangePattern.Match(postcodeRange.Trim());
if (!match.Success)
{
return false;
}
var left = match.Groups[1].Value.Trim();
var right = match.Groups[2].Value.Trim();
if (long.TryParse(zipCode, out var zip)
&& long.TryParse(left, out var leftValue)
&& long.TryParse(right, out var rightValue))
{
return zip > leftValue && zip <= rightValue;
}
return string.Compare(zipCode, left, StringComparison.OrdinalIgnoreCase) > 0
&& string.Compare(zipCode, right, StringComparison.OrdinalIgnoreCase) <= 0;
}
}

View File

@ -1,5 +1,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Oms.Application.Dtos.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.SystemAggregate.Consts;
@ -15,6 +17,12 @@ public static class OmsDocHelper
PropertyNameCaseInsensitive = true
};
private static readonly JsonSerializerOptions SnapshotJsonOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
/// <summary>
/// 将页码规范为至少 1。
/// </summary>
@ -93,6 +101,33 @@ public static class OmsDocHelper
return JsonSerializer.Serialize(value, JsonOptions);
}
/// <summary>
/// 序列化发布快照(忽略 null,避免写出旧兼容字段)。
/// </summary>
public static string? SerializeSnapshotJson<T>(T? value)
{
if (value is null)
{
return null;
}
return JsonSerializer.Serialize(value, SnapshotJsonOptions);
}
public static SalesProductRulesPublishSnapshotDto? DeserializeRulesSnapshot(string? json)
{
var snapshot = DeserializeJson<SalesProductRulesPublishSnapshotDto>(json);
PublishSnapshotNormalizer.NormalizeRulesSnapshot(snapshot);
return snapshot;
}
public static SalesProductPricePublishSnapshotDto? DeserializePriceSnapshot(string? json)
{
var snapshot = DeserializeJson<SalesProductPricePublishSnapshotDto>(json);
PublishSnapshotNormalizer.NormalizePriceSnapshot(snapshot);
return snapshot;
}
private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Object => element.EnumerateObject()

View File

@ -37,7 +37,7 @@ public static class PriceAreaSnapshotHelper
var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value, cancellationToken);
snapshot.PriceAreaName = area?.Name;
var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value, cancellationToken);
snapshot.PriceAreaInfos = infos.Select(MapInfoOutput).ToList();
snapshot.PriceAreaInfos = infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList();
}
internal static async Task<int?> ResolvePriceAreaIdAsync(
@ -74,19 +74,4 @@ public static class PriceAreaSnapshotHelper
return null;
}
private static KpiAreaInfoOutput MapInfoOutput(KpiAreaInfo entity) =>
new()
{
Id = entity.Id,
AreaId = entity.AreaId,
CountryCode = entity.CountryCode,
CountryName = entity.CountryName,
Province = entity.Province,
City = entity.City,
LeftPostcode = entity.LeftPostcode,
RightPostcode = entity.RightPostcode,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}

View File

@ -0,0 +1,50 @@
using Oms.Application.Dtos.Orders;
namespace Oms.Application.Shared;
/// <summary>
/// 发布快照读取后规范化(旧字段迁移、去掉冗余)。
/// </summary>
public static class PublishSnapshotNormalizer
{
public static void NormalizeRulesSnapshot(SalesProductRulesPublishSnapshotDto? snapshot)
{
if (snapshot is null)
{
return;
}
NormalizeKpiAreas(snapshot.BlacklistAreaInfos);
NormalizeKpiAreas(snapshot.RemoteAreaInfos);
}
public static void NormalizePriceSnapshot(SalesProductPricePublishSnapshotDto? snapshot)
{
if (snapshot is null)
{
return;
}
NormalizeKpiAreas(snapshot.PriceAreaInfos);
}
public static void NormalizeKpiAreas(List<KpiAreaSnapshotRuleDto>? areas)
{
if (areas is null)
{
return;
}
foreach (var area in areas)
{
if (string.IsNullOrWhiteSpace(area.PostcodeRange)
&& (!string.IsNullOrWhiteSpace(area.LeftPostcode) || !string.IsNullOrWhiteSpace(area.RightPostcode)))
{
area.PostcodeRange = KpiAreaSnapshotMapper.BuildPostcodeRange(area.LeftPostcode, area.RightPostcode);
}
area.LeftPostcode = null;
area.RightPostcode = null;
}
}
}

View File

@ -0,0 +1,63 @@
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Domain.ChannelsAggregate.Entities;
namespace Oms.Application.Shared;
/// <summary>
/// 发布快照瘦身:去掉审计时间等计费无用字段。
/// </summary>
public static class PublishSnapshotSlimmer
{
public static SalesProductPriceSnapshotItemDto SlimPrice(SalesProductPriceListOutput price) =>
new()
{
Id = price.Id,
SerialNumber = price.SerialNumber,
Status = price.Status,
PriceName = price.PriceName,
CostItem = price.CostItem,
CostUnit = price.CostUnit,
Currency = price.Currency,
ChargingTime = price.ChargingTime,
ChargingCondition = price.ChargingCondition,
Factors = price.Factors,
Details = price.Details
};
public static WeightRuleSnapshotRuleDto SlimWeightRule(WeightRuleInfo entity) =>
new()
{
Id = entity.Id,
Condition = entity.Condition,
BillableWeight = entity.BillableWeight,
VolumetricFactor = entity.VolumetricFactor
};
public static WeightRoundingSnapshotRangeDto SlimWeightRounding(WeightRoundingInfo entity) =>
new()
{
Id = entity.Id,
Start = entity.StartValue,
End = entity.EndValue,
Public = entity.Public
};
public static WeightRuleInfo ToWeightRuleEntity(WeightRuleSnapshotRuleDto rule) =>
new()
{
Id = rule.Id,
Condition = rule.Condition,
BillableWeight = rule.BillableWeight,
VolumetricFactor = rule.VolumetricFactor
};
public static WeightRoundingInfo ToWeightRoundingEntity(WeightRoundingSnapshotRangeDto range) =>
new()
{
Id = range.Id,
StartValue = range.Start,
EndValue = range.End,
Public = range.Public
};
}

View File

@ -1,4 +1,3 @@
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders;
using Oms.Domain.ChannelsAggregate.Entities;
@ -54,7 +53,7 @@ public static class RulesSnapshotHelper
/// 从快照内嵌黑名单/偏远明细匹配地址(不查库)。
/// </summary>
public static bool IsAreaHitFromSnapshot(
IReadOnlyList<KpiAreaInfoOutput>? infos,
IReadOnlyList<KpiAreaSnapshotRuleDto>? infos,
string countryCode,
string? province,
string? city,
@ -65,7 +64,7 @@ public static class RulesSnapshotHelper
return false;
}
return infos.Any(rule => ParcelBillingCalculator.IsAreaMatched(
return infos.Any(rule => KpiAreaSnapshotMapper.IsRuleMatched(
rule,
countryCode,
province,
@ -76,20 +75,20 @@ public static class RulesSnapshotHelper
/// <summary>
/// 将快照内嵌计重明细转为实体供计算器使用。
/// </summary>
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleInfoOutput> infos) =>
infos.Select(MapWeightRuleInfo).ToList();
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleSnapshotRuleDto> infos) =>
infos.Select(PublishSnapshotSlimmer.ToWeightRuleEntity).ToList();
/// <summary>
/// 将快照内嵌进位区间转为实体供计算器使用。
/// </summary>
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingInfoOutput> infos) =>
infos.Select(MapWeightRoundingInfo).ToList();
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingSnapshotRangeDto> infos) =>
infos.Select(PublishSnapshotSlimmer.ToWeightRoundingEntity).ToList();
private static async Task ApplyKpiAreaToSnapshotAsync(
SalesProductRulesPublishSnapshotDto snapshot,
int? areaId,
IKpiAreaRepository kpiAreaRepository,
Action<SalesProductRulesPublishSnapshotDto, int?, string?, List<KpiAreaInfoOutput>> assign,
Action<SalesProductRulesPublishSnapshotDto, int?, string?, List<KpiAreaSnapshotRuleDto>> assign,
CancellationToken cancellationToken)
{
if (!areaId.HasValue || areaId.Value <= 0)
@ -104,7 +103,7 @@ public static class RulesSnapshotHelper
snapshot,
areaId,
area?.Name,
infos.Select(MapKpiAreaInfo).ToList());
infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList());
}
private static async Task ApplyWeightRuleToSnapshotAsync(
@ -125,7 +124,7 @@ public static class RulesSnapshotHelper
var rule = await weightRuleRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRuleId.Value, cancellationToken);
snapshot.WeightRuleName = rule?.Name;
var infos = await weightRuleRepository.GetAllInfosByRuleIdAsync(product.WeightRuleId.Value, cancellationToken);
snapshot.WeightRuleInfos = infos.Select(MapWeightRuleInfoOutput).ToList();
snapshot.WeightRuleInfos = infos.Select(PublishSnapshotSlimmer.SlimWeightRule).ToList();
}
private static async Task ApplyWeightRoundingToSnapshotAsync(
@ -150,71 +149,6 @@ public static class RulesSnapshotHelper
var infos = await weightRoundingRepository.GetAllInfosByRoundingIdAsync(
product.WeightRoundingId.Value,
cancellationToken);
snapshot.WeightRoundingInfos = infos.Select(MapWeightRoundingInfoOutput).ToList();
snapshot.WeightRoundingInfos = infos.Select(PublishSnapshotSlimmer.SlimWeightRounding).ToList();
}
private static KpiAreaInfoOutput MapKpiAreaInfo(KpiAreaInfo entity) =>
new()
{
Id = entity.Id,
AreaId = entity.AreaId,
CountryCode = entity.CountryCode,
CountryName = entity.CountryName,
Province = entity.Province,
City = entity.City,
LeftPostcode = entity.LeftPostcode,
RightPostcode = entity.RightPostcode,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
private static WeightRuleInfoOutput MapWeightRuleInfoOutput(WeightRuleInfo entity) =>
new()
{
Id = entity.Id,
RuleId = entity.RuleId,
Client = entity.Client,
Condition = entity.Condition,
BillableWeight = entity.BillableWeight,
VolumetricFactor = entity.VolumetricFactor,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
private static WeightRoundingInfoOutput MapWeightRoundingInfoOutput(WeightRoundingInfo entity) =>
new()
{
Id = entity.Id,
RoundingId = entity.RoundingId,
StartValue = entity.StartValue,
EndValue = entity.EndValue,
Public = entity.Public,
CustomerGroup1 = entity.CustomerGroup1,
CustomerGroup2 = entity.CustomerGroup2,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
private static WeightRuleInfo MapWeightRuleInfo(WeightRuleInfoOutput output) =>
new()
{
Id = output.Id,
RuleId = output.RuleId,
Client = output.Client,
Condition = output.Condition,
BillableWeight = output.BillableWeight,
VolumetricFactor = output.VolumetricFactor
};
private static WeightRoundingInfo MapWeightRoundingInfo(WeightRoundingInfoOutput output) =>
new()
{
Id = output.Id,
RoundingId = output.RoundingId,
StartValue = output.StartValue,
EndValue = output.EndValue,
Public = output.Public,
CustomerGroup1 = output.CustomerGroup1,
CustomerGroup2 = output.CustomerGroup2
};
}

View File

@ -0,0 +1,23 @@
using FluentAssertions;
using Oms.Application.Shared;
namespace Oms.Tests;
public class KpiAreaSnapshotMapperTests
{
[Theory]
[InlineData("06000", "(00000,50000]", true)]
[InlineData("00000", "(00000,50000]", false)]
[InlineData("50000", "(00000,50000]", true)]
[InlineData("50001", "(00000,50000]", false)]
public void IsZipInLeftOpenRightClosedRange_ShouldMatchExpected(string zip, string range, bool expected)
{
KpiAreaSnapshotMapper.IsZipInLeftOpenRightClosedRange(zip, range).Should().Be(expected);
}
[Fact]
public void BuildPostcodeRange_ShouldFormatLeftOpenRightClosed()
{
KpiAreaSnapshotMapper.BuildPostcodeRange("00000", "50000").Should().Be("(00000,50000]");
}
}

View File

@ -2,6 +2,7 @@ using FluentAssertions;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Enums;
using Oms.Domain.OrdersAggregate.Consts;
@ -17,7 +18,7 @@ public class PriceCheckSnapshotVerificationTests
SalesProductId = 1,
Prices =
[
new SalesProductPriceListOutput
PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
{
Id = 1,
Status = 1,
@ -30,15 +31,15 @@ public class PriceCheckSnapshotVerificationTests
Detail(26, 1, "total", "3", "1,1.5", 13),
Detail(27, 1, "total", "3", "1.5,2", 14),
]
},
new SalesProductPriceListOutput
}),
PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
{
Id = 2,
Status = 1,
CostItem = "handling",
ChargingTime = "outbound",
Details = [Detail(28, 2, "total", "3", "0,10", 20)]
}
})
]
};
@ -90,7 +91,7 @@ public class PriceCheckSnapshotVerificationTests
SalesProductId = 1,
Prices =
[
new SalesProductPriceListOutput
PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
{
Id = 1,
Status = 1,
@ -100,14 +101,14 @@ public class PriceCheckSnapshotVerificationTests
Detail(34, 1, "total", "3", "(0-0.5]", 11),
Detail(35, 1, "total", "3", "(0.5-1]", 12),
]
},
new SalesProductPriceListOutput
}),
PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
{
Id = 2,
Status = 1,
CostItem = "handling",
Details = [Detail(38, 2, "total", "3", "(0-10]", 20)]
}
})
]
};
@ -151,23 +152,17 @@ public class PriceCheckSnapshotVerificationTests
PriceAreaName = "美国分区",
PriceAreaInfos =
[
new KpiAreaInfoOutput
new KpiAreaSnapshotRuleDto
{
Id = 4,
AreaId = 3,
CountryCode = "US",
CountryName = "美国",
LeftPostcode = "50000",
RightPostcode = "90000"
PostcodeRange = "(50000,90000]"
},
new KpiAreaInfoOutput
new KpiAreaSnapshotRuleDto
{
Id = 3,
AreaId = 3,
CountryCode = "US",
CountryName = "美国",
LeftPostcode = "00000",
RightPostcode = "50000"
PostcodeRange = "(00000,50000]"
}
]
};

View File

@ -79,10 +79,14 @@ public class RulesSnapshotHelperPublishTests
snapshot.WeightRoundingId.Should().Be(2);
snapshot.WeightRoundingInfos.Should().HaveCount(1);
var json = OmsDocHelper.SerializeJson(snapshot);
json.Should().Contain("BlacklistAreaInfos");
var json = OmsDocHelper.SerializeSnapshotJson(snapshot);
json.Should().Contain("PostcodeRange");
json.Should().Contain("(90000,90010]");
json.Should().Contain("WeightRuleInfos");
json.Should().Contain("WeightRoundingInfos");
json.Should().Contain("RemoteAreaInfos");
json.Should().NotContain("CreateTime");
json.Should().NotContain("UpdateTime");
json.Should().NotContain("LeftPostcode");
}
}

View File

@ -1,5 +1,4 @@
using FluentAssertions;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders;
using Oms.Application.Shared;
@ -18,34 +17,29 @@ public class RulesSnapshotVerificationTests
BlacklistAreaId = 10,
BlacklistAreaInfos =
[
new KpiAreaInfoOutput
new KpiAreaSnapshotRuleDto
{
Id = 1,
AreaId = 10,
CountryCode = "US",
LeftPostcode = "90000",
RightPostcode = "90010"
PostcodeRange = "(90000,90010]"
}
],
RemoteAreaId = 20,
RemoteAreaInfos =
[
new KpiAreaInfoOutput
new KpiAreaSnapshotRuleDto
{
Id = 2,
AreaId = 20,
CountryCode = "US",
LeftPostcode = "06000",
RightPostcode = "06999"
PostcodeRange = "(05999,06999]"
}
],
WeightRuleId = 1,
WeightRuleInfos =
[
new WeightRuleInfoOutput
new WeightRuleSnapshotRuleDto
{
Id = 1,
RuleId = 1,
VolumetricFactor = "6000",
Condition = "gw>vw",
BillableWeight = "gw"
@ -54,12 +48,11 @@ public class RulesSnapshotVerificationTests
WeightRoundingId = 1,
WeightRoundingInfos =
[
new WeightRoundingInfoOutput
new WeightRoundingSnapshotRangeDto
{
Id = 1,
RoundingId = 1,
StartValue = 0,
EndValue = 30,
Start = 0,
End = 30,
Public = "0.5"
}
]

View File

@ -1,6 +1,7 @@
using System.Globalization;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
namespace Oms.Tests.Support;
@ -13,7 +14,11 @@ internal static class ParcelBillingTestData
public static SalesProductPricePublishSnapshotDto BuildReceivableSnapshot(
params SalesProductPriceListOutput[] prices) =>
new() { SalesProductId = 1, Prices = prices.ToList() };
new()
{
SalesProductId = 1,
Prices = prices.Select(PublishSnapshotSlimmer.SlimPrice).ToList()
};
public static List<ServiceChannelPriceListOutput> BuildPayablePrices(
params ServiceChannelPriceListOutput[] prices) => prices.ToList();