feat: 渠道买价/规则发布钉 KPI 分区包,排货应付按渠道价区解析
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
各渠道异构价区与偏远不再复用产品卖价 zoneKeys;发布只存包引用,计费 hydrate 后硬失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bb8c0a906c
commit
35156eedf4
@ -0,0 +1,22 @@
|
||||
namespace Oms.Application.Dtos.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道发布同步态(轻量,不含快照正文)。
|
||||
/// </summary>
|
||||
public class ServiceChannelPublishSyncStatusOutput
|
||||
{
|
||||
/// <summary>相对最新生效版本是否有待发布变更。</summary>
|
||||
public bool Pending { get; set; }
|
||||
|
||||
/// <summary>最新生效版本号;无则 null。</summary>
|
||||
public string? OnlineVersion { get; set; }
|
||||
|
||||
/// <summary>最新生效发布时间。</summary>
|
||||
public DateTime? OnlinePublishTime { get; set; }
|
||||
|
||||
/// <summary>待发布快照内容哈希(规范化后)。</summary>
|
||||
public string? PendingContentHash { get; set; }
|
||||
|
||||
/// <summary>线上快照内容哈希。</summary>
|
||||
public string? OnlineContentHash { get; set; }
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Dtos.Channels;
|
||||
|
||||
/// <summary>
|
||||
@ -8,4 +11,17 @@ public class ServiceChannelRulesPublishSnapshotDto
|
||||
public ServiceChannelSaveInput? Channel { get; set; }
|
||||
|
||||
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
|
||||
|
||||
/// <summary>偏远分区 ID。</summary>
|
||||
public int? RemoteAreaId { get; set; }
|
||||
|
||||
/// <summary>偏远分区名称。</summary>
|
||||
public string? RemoteAreaName { get; set; }
|
||||
|
||||
/// <summary>偏远分区包引用(规则发布时钉包)。</summary>
|
||||
public KpiAreaPackRefDto? RemoteAreaRef { get; set; }
|
||||
|
||||
/// <summary>运行时 hydrate / LIVE 用,不落发布 JSON。</summary>
|
||||
[JsonIgnore]
|
||||
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
|
||||
}
|
||||
|
||||
@ -129,12 +129,25 @@ public class SalesProductRulesPublishSnapshotDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道买价发布快照。
|
||||
/// 服务渠道买价发布快照(渠道自有价区,与产品卖价分区独立)。
|
||||
/// </summary>
|
||||
public class ServiceChannelPricePublishSnapshotDto
|
||||
{
|
||||
public int ServiceChannelId { get; set; }
|
||||
|
||||
/// <summary>价格分区 ID(单表时冗余展示;以 PriceAreaRefs 为准)。</summary>
|
||||
public int? PriceAreaId { get; set; }
|
||||
|
||||
/// <summary>价格分区名称。</summary>
|
||||
public string? PriceAreaName { get; set; }
|
||||
|
||||
/// <summary>渠道价区包引用(买价发布时钉包)。</summary>
|
||||
public List<KpiAreaPackRefDto> PriceAreaRefs { get; set; } = [];
|
||||
|
||||
/// <summary>运行时 hydrate / LIVE 用,不落发布 JSON。</summary>
|
||||
[JsonIgnore]
|
||||
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
|
||||
|
||||
public List<ServiceChannelPriceSnapshotItemDto> Prices { get; set; } = [];
|
||||
}
|
||||
|
||||
|
||||
@ -185,13 +185,16 @@ public partial class SalesProductService
|
||||
payableFees = await TrySimulatePayableFeesAsync(
|
||||
product,
|
||||
rulesSnapshot,
|
||||
zoneKeys,
|
||||
chargeableWeight,
|
||||
cargoAttribute,
|
||||
input.WarehouseId,
|
||||
warehouseName,
|
||||
isRemote,
|
||||
remoteZoneKeys,
|
||||
countryCode,
|
||||
input.Province,
|
||||
input.City,
|
||||
input.ZipCode,
|
||||
input.DeclaredSum,
|
||||
input.DeclaredCurrency,
|
||||
priceVersion,
|
||||
@ -316,13 +319,16 @@ public partial class SalesProductService
|
||||
private async Task<List<SalesProductBillingSimulateFeeOutput>> TrySimulatePayableFeesAsync(
|
||||
SalesProduct product,
|
||||
SalesProductRulesPublishSnapshotDto rulesSnapshot,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
decimal chargeableWeight,
|
||||
string cargoAttribute,
|
||||
int? warehouseId,
|
||||
string? warehouseName,
|
||||
bool isRemote,
|
||||
IReadOnlyList<string> remoteZoneKeys,
|
||||
bool productIsRemote,
|
||||
IReadOnlyList<string> productRemoteZoneKeys,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode,
|
||||
decimal? declaredSum,
|
||||
string? declaredCurrency,
|
||||
string priceVersion,
|
||||
@ -344,7 +350,7 @@ public partial class SalesProductService
|
||||
WarehouseId = warehouseId,
|
||||
DeclaredSum = declaredSum,
|
||||
DeclaredCurrency = declaredCurrency,
|
||||
IsRemote = isRemote ? (byte)1 : (byte)0
|
||||
IsRemote = productIsRemote ? (byte)1 : (byte)0
|
||||
})).Passed;
|
||||
var provider = filtered.OrderBy(x => x.SerialNumber).FirstOrDefault(x => x.ServiceChannelId is > 0);
|
||||
if (provider?.ServiceChannelId is not > 0)
|
||||
@ -353,33 +359,81 @@ public partial class SalesProductService
|
||||
return [];
|
||||
}
|
||||
|
||||
var channelPrices = await BuildChannelPriceOutputsAsync(provider.ServiceChannelId.Value);
|
||||
if (channelPrices.Count == 0)
|
||||
var channelId = provider.ServiceChannelId.Value;
|
||||
var channelPriceSnapshot = new ServiceChannelPricePublishSnapshotDto
|
||||
{
|
||||
warnings.Add($"服务渠道 {provider.ServiceChannelId} 无买价,跳过应付估算");
|
||||
ServiceChannelId = channelId,
|
||||
Prices = (await BuildChannelPriceOutputsAsync(channelId))
|
||||
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||
.ToList()
|
||||
};
|
||||
if (channelPriceSnapshot.Prices.Count == 0)
|
||||
{
|
||||
warnings.Add($"服务渠道 {channelId} 无买价,跳过应付估算");
|
||||
return [];
|
||||
}
|
||||
|
||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == provider.ServiceChannelId.Value);
|
||||
var payableRemoteZoneKeys = isRemote
|
||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
||||
channel?.RemoteAreaId is > 0 ? channel.RemoteAreaId : null)
|
||||
: Array.Empty<string>();
|
||||
// 渠道未配偏远分区时回落产品偏远键,便于模拟偏远费行
|
||||
if (isRemote && payableRemoteZoneKeys.Count == 0)
|
||||
await PriceAreaSnapshotHelper.ApplyLiveChannelPriceAreaInfosAsync(
|
||||
channelPriceSnapshot,
|
||||
KpiAreaRepository,
|
||||
KpiAreaPackService);
|
||||
var channelPrices = channelPriceSnapshot.Prices
|
||||
.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput)
|
||||
.ToList();
|
||||
|
||||
IReadOnlyList<string> payableZoneKeys = Array.Empty<string>();
|
||||
if (channelPriceSnapshot.PriceAreaInfos is { Count: > 0 })
|
||||
{
|
||||
payableRemoteZoneKeys = remoteZoneKeys;
|
||||
var resolved = ParcelBillingCalculator.TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||
channelPriceSnapshot,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
if (resolved == null || resolved.Count == 0)
|
||||
{
|
||||
warnings.Add($"服务渠道 {channelId} 买价分区未匹配,跳过应付估算");
|
||||
return [];
|
||||
}
|
||||
|
||||
payableZoneKeys = resolved;
|
||||
}
|
||||
|
||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
|
||||
var channelRemoteAreaId = channel?.RemoteAreaId;
|
||||
var channelIsRemote = productIsRemote;
|
||||
if (channelRemoteAreaId is > 0)
|
||||
{
|
||||
var channelRules = new ServiceChannelRulesPublishSnapshotDto();
|
||||
await KpiAreaPackService.LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||
channelRules,
|
||||
channelRemoteAreaId);
|
||||
channelIsRemote = GeoPartitionMatcher.IsHit(
|
||||
channelRules.RemoteAreaInfos,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
}
|
||||
|
||||
var payableRemoteZoneKeys = channelIsRemote
|
||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
||||
channelRemoteAreaId is > 0 ? channelRemoteAreaId : null)
|
||||
: Array.Empty<string>();
|
||||
if (channelIsRemote && payableRemoteZoneKeys.Count == 0)
|
||||
{
|
||||
payableRemoteZoneKeys = productRemoteZoneKeys;
|
||||
}
|
||||
|
||||
var payableContext = OrderRuleEvaluationContext.FromOrderOnly(new Order
|
||||
{
|
||||
ProductCode = product.ProductCode,
|
||||
ConsigneeCountry = null,
|
||||
ConsigneeCountry = countryCode,
|
||||
CargoAttribute = cargoAttribute,
|
||||
WarehouseId = warehouseId,
|
||||
DeclaredSum = declaredSum,
|
||||
DeclaredCurrency = declaredCurrency,
|
||||
IsRemote = isRemote ? (byte)1 : (byte)0
|
||||
IsRemote = channelIsRemote ? (byte)1 : (byte)0
|
||||
});
|
||||
|
||||
var payable = ParcelBillingCalculator.CalculatePayableFees(
|
||||
@ -387,7 +441,7 @@ public partial class SalesProductService
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
payableZoneKeys,
|
||||
cargoAttribute,
|
||||
OrderFeeChargingStageCodes.Forecast,
|
||||
warehouseId,
|
||||
@ -399,11 +453,11 @@ public partial class SalesProductService
|
||||
|
||||
if (payable.Count == 0)
|
||||
{
|
||||
warnings.Add($"服务渠道 {provider.ServiceChannelId} 买价未匹配,跳过应付估算");
|
||||
warnings.Add($"服务渠道 {channelId} 买价未匹配,跳过应付估算");
|
||||
return [];
|
||||
}
|
||||
|
||||
warnings.Add($"应付按排货服务渠道 {provider.ServiceChannelId} 估算(首个可用渠道)");
|
||||
warnings.Add($"应付按排货服务渠道 {channelId} 估算(首个可用渠道,渠道价区)");
|
||||
return payable.Select(x => new SalesProductBillingSimulateFeeOutput
|
||||
{
|
||||
CostItem = x.CostItem,
|
||||
|
||||
@ -243,23 +243,29 @@ public partial class ServiceChannelService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装买价发布快照(不落库),与产品卖价 §5.3.2 对称。
|
||||
/// 组装买价发布快照(不落库):钉渠道自有价区包。
|
||||
/// </summary>
|
||||
private async Task<ServiceChannelPricePublishSnapshot> BuildPricePublishSnapshotAsync(int serviceChannelId)
|
||||
private async Task<ServiceChannelPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int serviceChannelId)
|
||||
{
|
||||
return new ServiceChannelPricePublishSnapshot
|
||||
var snapshot = new ServiceChannelPricePublishSnapshotDto
|
||||
{
|
||||
ServiceChannelId = serviceChannelId,
|
||||
Prices = (await GetPriceOutputsAsync(serviceChannelId))
|
||||
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||
.ToList()
|
||||
};
|
||||
await PriceAreaSnapshotHelper.ApplyChannelPriceAreaToSnapshotAsync(
|
||||
snapshot,
|
||||
KpiAreaRepository,
|
||||
KpiAreaPackService,
|
||||
ResolvePublishUser());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装渠道规则发布快照(不落库)。
|
||||
/// 组装渠道规则发布快照(不落库):钉偏远分区包。
|
||||
/// </summary>
|
||||
private async Task<ServiceChannelRulesPublishSnapshot> BuildRulesPublishSnapshotAsync(int serviceChannelId)
|
||||
private async Task<ServiceChannelRulesPublishSnapshotDto> BuildRulesPublishSnapshotAsync(int serviceChannelId)
|
||||
{
|
||||
var entity = await GetEntityOrThrowAsync(serviceChannelId);
|
||||
ServiceChannelDownstreamSnapshotDto? downstream = null;
|
||||
@ -272,13 +278,39 @@ public partial class ServiceChannelService
|
||||
}
|
||||
}
|
||||
|
||||
return new ServiceChannelRulesPublishSnapshot
|
||||
var snapshot = new ServiceChannelRulesPublishSnapshotDto
|
||||
{
|
||||
Channel = MapChannelToSaveInput(entity),
|
||||
Downstream = downstream
|
||||
Downstream = downstream,
|
||||
RemoteAreaInfos = []
|
||||
};
|
||||
|
||||
if (entity.RemoteAreaId is > 0)
|
||||
{
|
||||
var packRef = await KpiAreaPackService.EnsurePackAsync(
|
||||
entity.RemoteAreaId.Value,
|
||||
KpiAreaPackService.AreaTypeRemote,
|
||||
ResolvePublishUser(),
|
||||
description: "service-channel-rules-publish");
|
||||
snapshot.RemoteAreaId = entity.RemoteAreaId;
|
||||
snapshot.RemoteAreaName = packRef.AreaName;
|
||||
snapshot.RemoteAreaRef = packRef;
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.RemoteAreaId = null;
|
||||
snapshot.RemoteAreaName = null;
|
||||
snapshot.RemoteAreaRef = null;
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private string ResolvePublishUser() =>
|
||||
string.IsNullOrWhiteSpace(CurrentUser.UserName)
|
||||
? CurrentUser.GetId<int>().ToString()
|
||||
: CurrentUser.UserName!;
|
||||
|
||||
private static ServiceChannelSaveInput MapChannelToSaveInput(ServiceChannel entity) =>
|
||||
new()
|
||||
{
|
||||
@ -334,7 +366,7 @@ public partial class ServiceChannelService
|
||||
|
||||
if (operatorType == PriceOperatorType)
|
||||
{
|
||||
var snapshot = OmsDocHelper.DeserializeJson<ServiceChannelPricePublishSnapshot>(record.Content);
|
||||
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(record.Content);
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new Fake.Application.BusinessException("发布快照无效");
|
||||
@ -349,8 +381,8 @@ public partial class ServiceChannelService
|
||||
return;
|
||||
}
|
||||
|
||||
var rulesSnapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshot>(record.Content);
|
||||
if (rulesSnapshot == null)
|
||||
var rulesSnapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(record.Content);
|
||||
if (rulesSnapshot?.Channel == null)
|
||||
{
|
||||
throw new Fake.Application.BusinessException("发布快照无效");
|
||||
}
|
||||
@ -658,17 +690,4 @@ public partial class ServiceChannelService
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class ServiceChannelPricePublishSnapshot
|
||||
{
|
||||
public int ServiceChannelId { get; set; }
|
||||
|
||||
public List<ServiceChannelPriceSnapshotItemDto> Prices { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class ServiceChannelRulesPublishSnapshot
|
||||
{
|
||||
public ServiceChannelSaveInput Channel { get; set; } = new();
|
||||
|
||||
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ public partial class ServiceChannelService : ApplicationService
|
||||
|
||||
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||||
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||
private KpiAreaPackService KpiAreaPackService => LazyServiceProvider.GetRequiredService<KpiAreaPackService>();
|
||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||
@ -400,6 +402,63 @@ public partial class ServiceChannelService : ApplicationService
|
||||
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。与产品卖价对称。
|
||||
/// </summary>
|
||||
[HttpGet("/channel/service-channel/publish/sync-status")]
|
||||
public async Task<ServiceChannelPublishSyncStatusOutput> GetPublishSyncStatusAsync(
|
||||
[FromQuery] int serviceChannelId,
|
||||
[FromQuery] string operatorType)
|
||||
{
|
||||
var normalized = NormalizeOperatorType(operatorType);
|
||||
await GetEntityOrThrowAsync(serviceChannelId);
|
||||
|
||||
object pendingSnapshot = normalized == PriceOperatorType
|
||||
? await BuildPricePublishSnapshotAsync(serviceChannelId)
|
||||
: await BuildRulesPublishSnapshotAsync(serviceChannelId);
|
||||
var pendingJson = OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty;
|
||||
var pendingHash = ComputeContentHash(pendingJson);
|
||||
|
||||
var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync(
|
||||
serviceChannelId,
|
||||
normalized,
|
||||
DateTime.Now);
|
||||
if (onlineRecord == null)
|
||||
{
|
||||
return new ServiceChannelPublishSyncStatusOutput
|
||||
{
|
||||
Pending = true,
|
||||
OnlineVersion = null,
|
||||
OnlinePublishTime = null,
|
||||
PendingContentHash = pendingHash,
|
||||
OnlineContentHash = null
|
||||
};
|
||||
}
|
||||
|
||||
var onlineJson = onlineRecord.Content ?? string.Empty;
|
||||
object? onlineNormalized = normalized == PriceOperatorType
|
||||
? OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson)
|
||||
: OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(onlineJson);
|
||||
var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty;
|
||||
var onlineHash = ComputeContentHash(onlineCanon);
|
||||
|
||||
return new ServiceChannelPublishSyncStatusOutput
|
||||
{
|
||||
Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal),
|
||||
OnlineVersion = onlineRecord.Version,
|
||||
OnlinePublishTime = onlineRecord.PublishTime,
|
||||
PendingContentHash = pendingHash,
|
||||
OnlineContentHash = onlineHash
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeContentHash(string content)
|
||||
{
|
||||
var hash = global::System.Security.Cryptography.SHA256.HashData(
|
||||
global::System.Text.Encoding.UTF8.GetBytes(content));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询服务渠道发布规则列表。
|
||||
/// </summary>
|
||||
|
||||
@ -1057,8 +1057,9 @@ public partial class OrderService
|
||||
continue;
|
||||
}
|
||||
|
||||
var channelIsRemote = await IsChannelRemoteHitAsync(
|
||||
channel,
|
||||
var channelRulesBinding = await ResolveChannelRulesBindingAsync(channel, bindTime);
|
||||
var channelIsRemote = IsChannelRemoteHit(
|
||||
channelRulesBinding,
|
||||
order.ConsigneeCountry ?? string.Empty,
|
||||
recipient?.Province,
|
||||
recipient?.City,
|
||||
@ -1068,15 +1069,38 @@ public partial class OrderService
|
||||
channelIsRemote ? (byte)1 : (byte)0);
|
||||
|
||||
var channelPriceBinding = await ResolveChannelPriceBindingAsync(channel.Id, bindTime);
|
||||
var channelZoneKeys = ResolveChannelZoneKeysOrSkip(
|
||||
channelPriceBinding,
|
||||
order.ConsigneeCountry ?? string.Empty,
|
||||
recipient?.Province,
|
||||
recipient?.City,
|
||||
recipient?.ZipCode,
|
||||
out var channelZoneMissReason);
|
||||
if (channelZoneKeys == null)
|
||||
{
|
||||
skipped.Add(new OrderAllocationSkippedOutput
|
||||
{
|
||||
ServiceChannelId = channel.Id,
|
||||
ChannelCode = channel.ChannelCode,
|
||||
ChannelName = channel.ChannelName,
|
||||
Stage = AllocationResolver.SkipStageBuyPrice,
|
||||
MatchCondition = AllocationResolver.TruncateMatchCondition(provider.MatchCondition),
|
||||
Reason = channelZoneMissReason
|
||||
?? $"渠道买价分区未匹配(版本 {channelPriceBinding.PriceVersion})"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var channelRemoteZoneKeys = channelIsRemote
|
||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(channel.RemoteAreaId)
|
||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
||||
channelRulesBinding.RemoteAreaId ?? channel.RemoteAreaId)
|
||||
: Array.Empty<string>();
|
||||
var previewFees = ParcelBillingCalculator.CalculatePayableFees(
|
||||
channelPriceBinding.Prices,
|
||||
channelPriceBinding.PriceVersion,
|
||||
channelPriceBinding.PriceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
channelZoneKeys,
|
||||
cargoAttribute,
|
||||
chargingStage,
|
||||
order.WarehouseId,
|
||||
@ -1090,7 +1114,7 @@ public partial class OrderService
|
||||
var miss = ParcelBillingCalculator.ExplainPayablePriceMiss(
|
||||
channelPriceBinding.Prices,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
channelZoneKeys,
|
||||
cargoAttribute,
|
||||
order.WarehouseId,
|
||||
warehouseName,
|
||||
@ -1369,6 +1393,15 @@ public partial class OrderService
|
||||
public string PriceVersion { get; init; } = string.Empty;
|
||||
|
||||
public DateTime PriceEffectiveTime { get; init; }
|
||||
|
||||
public ServiceChannelPricePublishSnapshotDto Snapshot { get; init; } = new();
|
||||
}
|
||||
|
||||
private sealed class ChannelRulesBinding
|
||||
{
|
||||
public int? RemoteAreaId { get; init; }
|
||||
|
||||
public ServiceChannelRulesPublishSnapshotDto Snapshot { get; init; } = new();
|
||||
}
|
||||
|
||||
private async Task<ChannelPriceBinding> ResolveChannelPriceBindingAsync(int serviceChannelId, DateTime bindTime)
|
||||
@ -1382,23 +1415,157 @@ public partial class OrderService
|
||||
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(priceRecord.Content);
|
||||
if (snapshot?.Prices.Count > 0)
|
||||
{
|
||||
await KpiAreaPackService.HydrateChannelPriceSnapshotAsync(snapshot);
|
||||
return new ChannelPriceBinding
|
||||
{
|
||||
Prices = snapshot.Prices.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput).ToList(),
|
||||
PriceVersion = priceRecord.Version,
|
||||
PriceEffectiveTime = priceRecord.PublishTime
|
||||
PriceEffectiveTime = priceRecord.PublishTime,
|
||||
Snapshot = snapshot
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var liveSnapshot = await BuildLiveChannelPriceSnapshotAsync(serviceChannelId);
|
||||
return new ChannelPriceBinding
|
||||
{
|
||||
Prices = await BuildChannelPriceOutputsAsync(serviceChannelId),
|
||||
Prices = liveSnapshot.Prices.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput).ToList(),
|
||||
PriceVersion = $"LIVE-{serviceChannelId}",
|
||||
PriceEffectiveTime = bindTime
|
||||
PriceEffectiveTime = bindTime,
|
||||
Snapshot = liveSnapshot
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ChannelRulesBinding> ResolveChannelRulesBindingAsync(
|
||||
ServiceChannel channel,
|
||||
DateTime bindTime)
|
||||
{
|
||||
var rulesRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync(
|
||||
channel.Id,
|
||||
RulesOperatorType,
|
||||
bindTime);
|
||||
if (rulesRecord?.Content != null)
|
||||
{
|
||||
var snapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(rulesRecord.Content);
|
||||
if (snapshot != null)
|
||||
{
|
||||
await KpiAreaPackService.HydrateChannelRulesSnapshotAsync(snapshot);
|
||||
return new ChannelRulesBinding
|
||||
{
|
||||
RemoteAreaId = snapshot.RemoteAreaRef?.AreaId
|
||||
?? snapshot.RemoteAreaId
|
||||
?? snapshot.Channel?.RemoteAreaId
|
||||
?? channel.RemoteAreaId,
|
||||
Snapshot = snapshot
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var live = new ServiceChannelRulesPublishSnapshotDto
|
||||
{
|
||||
Channel = new ServiceChannelSaveInput
|
||||
{
|
||||
Id = channel.Id,
|
||||
RemoteAreaId = channel.RemoteAreaId
|
||||
},
|
||||
RemoteAreaInfos = []
|
||||
};
|
||||
await KpiAreaPackService.LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||
live,
|
||||
channel.RemoteAreaId);
|
||||
return new ChannelRulesBinding
|
||||
{
|
||||
RemoteAreaId = channel.RemoteAreaId,
|
||||
Snapshot = live
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ServiceChannelPricePublishSnapshotDto> BuildLiveChannelPriceSnapshotAsync(
|
||||
int serviceChannelId)
|
||||
{
|
||||
var snapshot = new ServiceChannelPricePublishSnapshotDto
|
||||
{
|
||||
ServiceChannelId = serviceChannelId,
|
||||
Prices = (await BuildChannelPriceOutputsAsync(serviceChannelId))
|
||||
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||
.ToList()
|
||||
};
|
||||
await PriceAreaSnapshotHelper.ApplyLiveChannelPriceAreaInfosAsync(
|
||||
snapshot,
|
||||
KpiAreaRepository,
|
||||
KpiAreaPackService);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按渠道买价价区包解析 zoneKeys;有价区配置但未命中时返回 null(跳过该渠道)。
|
||||
/// 无价区配置时返回空列表(不回落产品卖价分区)。
|
||||
/// </summary>
|
||||
private static IReadOnlyList<string>? ResolveChannelZoneKeysOrSkip(
|
||||
ChannelPriceBinding binding,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode,
|
||||
out string? missReason)
|
||||
{
|
||||
missReason = null;
|
||||
var snapshot = binding.Snapshot;
|
||||
if (snapshot.PriceAreaRefs is { Count: > 0 }
|
||||
&& snapshot.PriceAreaInfos is not { Count: > 0 })
|
||||
{
|
||||
missReason =
|
||||
$"渠道价格分区包未加载: refs={snapshot.PriceAreaRefs.Count} version={binding.PriceVersion}";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (snapshot.PriceAreaInfos is not { Count: > 0 })
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var keys = ParcelBillingCalculator.TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||
snapshot,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
if (keys == null || keys.Count == 0)
|
||||
{
|
||||
missReason = $"渠道买价分区未匹配(版本 {binding.PriceVersion})";
|
||||
return null;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static bool IsChannelRemoteHit(
|
||||
ChannelRulesBinding binding,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode)
|
||||
{
|
||||
var snapshot = binding.Snapshot;
|
||||
if (snapshot.RemoteAreaRef != null && snapshot.RemoteAreaInfos is not { Count: > 0 })
|
||||
{
|
||||
throw new BusinessException(
|
||||
$"渠道偏远分区包未加载: areaId={snapshot.RemoteAreaRef.AreaId} version={snapshot.RemoteAreaRef.PackVersion}");
|
||||
}
|
||||
|
||||
if (snapshot.RemoteAreaInfos is { Count: > 0 })
|
||||
{
|
||||
return GeoPartitionMatcher.IsHit(
|
||||
snapshot.RemoteAreaInfos,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
|
||||
{
|
||||
var snapshot = new SalesProductRulesPublishSnapshotDto
|
||||
@ -2970,19 +3137,4 @@ public partial class OrderService
|
||||
return OrderRuleEvaluationContext.FromOrderOnly(evalOrder);
|
||||
}
|
||||
|
||||
private async Task<bool> IsChannelRemoteHitAsync(
|
||||
ServiceChannel channel,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode)
|
||||
{
|
||||
if (channel.RemoteAreaId is not > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(channel.RemoteAreaId.Value);
|
||||
return GeoPartitionMatcher.IsHit(rules, countryCode, province, city, zipCode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,15 +107,53 @@ internal static class ParcelBillingCalculator
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode) =>
|
||||
TryResolveZoneKeysFromAreaInfos(
|
||||
priceSnapshot?.PriceAreaInfos,
|
||||
priceSnapshot?.PriceAreaId,
|
||||
priceSnapshot?.PriceAreaName,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
|
||||
/// <summary>
|
||||
/// 从渠道买价快照分区规则解析分区键(不查库;与产品卖价独立)。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string>? TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||
ServiceChannelPricePublishSnapshotDto? priceSnapshot,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode) =>
|
||||
TryResolveZoneKeysFromAreaInfos(
|
||||
priceSnapshot?.PriceAreaInfos,
|
||||
priceSnapshot?.PriceAreaId,
|
||||
priceSnapshot?.PriceAreaName,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
zipCode);
|
||||
|
||||
/// <summary>
|
||||
/// 按分区规则列表匹配地址并生成 zone 键。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string>? TryResolveZoneKeysFromAreaInfos(
|
||||
IReadOnlyList<KpiAreaSnapshotRuleDto>? priceAreaInfos,
|
||||
int? priceAreaId,
|
||||
string? priceAreaName,
|
||||
string countryCode,
|
||||
string? province,
|
||||
string? city,
|
||||
string? zipCode)
|
||||
{
|
||||
if (priceSnapshot?.PriceAreaInfos == null || priceSnapshot.PriceAreaInfos.Count == 0)
|
||||
if (priceAreaInfos == null || priceAreaInfos.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var matched = GeoPartitionMatcher.MatchSnapshot(
|
||||
priceSnapshot.PriceAreaInfos,
|
||||
priceAreaInfos,
|
||||
countryCode,
|
||||
province,
|
||||
city,
|
||||
@ -125,11 +163,10 @@ internal static class ParcelBillingCalculator
|
||||
return null;
|
||||
}
|
||||
|
||||
// 多价格分区表时优先用规则自身 AreaId,避免落到快照单一 PriceAreaId
|
||||
var areaId = matched.AreaId > 0 ? matched.AreaId : priceSnapshot.PriceAreaId;
|
||||
var areaId = matched.AreaId > 0 ? matched.AreaId : priceAreaId;
|
||||
var areaName = !string.IsNullOrWhiteSpace(matched.AreaName)
|
||||
? matched.AreaName
|
||||
: priceSnapshot.PriceAreaName;
|
||||
: priceAreaName;
|
||||
var keys = BuildPriceZoneKeys(matched, areaName, areaId);
|
||||
return keys.Count > 0 ? keys : null;
|
||||
}
|
||||
@ -476,10 +513,22 @@ internal static class ParcelBillingCalculator
|
||||
/// <summary>
|
||||
/// 收集卖价快照中所有启用价格项的明细分区键(去重)。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot)
|
||||
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||
CollectDistinctDetailZoneKeys(
|
||||
priceSnapshot.Prices.Select(p => (p.Status, p.Details)));
|
||||
|
||||
/// <summary>
|
||||
/// 收集渠道买价快照中所有启用价格项的明细分区键(去重)。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(ServiceChannelPricePublishSnapshotDto priceSnapshot) =>
|
||||
CollectDistinctDetailZoneKeys(
|
||||
priceSnapshot.Prices.Select(p => (p.Status, p.Details)));
|
||||
|
||||
private static IReadOnlyList<string> CollectDistinctDetailZoneKeys(
|
||||
IEnumerable<(byte Status, List<ChannelPriceDetailOutput>? Details)> prices)
|
||||
{
|
||||
var zones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var price in priceSnapshot.Prices.Where(x => EnableStatusRules.IsEnabled(x.Status)))
|
||||
foreach (var price in prices.Where(x => EnableStatusRules.IsEnabled(x.Status)))
|
||||
{
|
||||
foreach (var detail in price.Details ?? [])
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Text;
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
@ -254,6 +255,131 @@ public class KpiAreaPackService : IScopedDependency
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道买价:Refs hydrate 到 Infos。
|
||||
/// </summary>
|
||||
public async Task HydrateChannelPriceSnapshotAsync(
|
||||
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (snapshot.PriceAreaRefs is not { Count: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||
var seen = new HashSet<int>();
|
||||
foreach (var packRef in snapshot.PriceAreaRefs)
|
||||
{
|
||||
var rules = await GetRulesOrThrowAsync(packRef, cancellationToken);
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (seen.Add(rule.Id))
|
||||
{
|
||||
merged.Add(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.PriceAreaInfos = merged;
|
||||
if (snapshot.PriceAreaRefs.Count == 1)
|
||||
{
|
||||
snapshot.PriceAreaId = snapshot.PriceAreaRefs[0].AreaId;
|
||||
snapshot.PriceAreaName = snapshot.PriceAreaRefs[0].AreaName;
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.PriceAreaId = null;
|
||||
snapshot.PriceAreaName = string.Join(
|
||||
"、",
|
||||
snapshot.PriceAreaRefs
|
||||
.Select(x => x.AreaName)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道规则:偏远 Ref hydrate。
|
||||
/// </summary>
|
||||
public async Task HydrateChannelRulesSnapshotAsync(
|
||||
ServiceChannelRulesPublishSnapshotDto snapshot,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (snapshot.RemoteAreaRef != null)
|
||||
{
|
||||
snapshot.RemoteAreaInfos = await GetRulesOrThrowAsync(snapshot.RemoteAreaRef, cancellationToken);
|
||||
snapshot.RemoteAreaId = snapshot.RemoteAreaRef.AreaId;
|
||||
snapshot.RemoteAreaName = snapshot.RemoteAreaRef.AreaName;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadLiveInfosIntoChannelPriceSnapshotAsync(
|
||||
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||
IReadOnlyList<int> areaIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||
var seen = new HashSet<int>();
|
||||
var names = new List<string>();
|
||||
foreach (var areaId in areaIds.Where(id => id > 0).Distinct())
|
||||
{
|
||||
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(area?.Name))
|
||||
{
|
||||
names.Add(area.Name.Trim());
|
||||
}
|
||||
|
||||
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken);
|
||||
foreach (var info in infos)
|
||||
{
|
||||
if (!seen.Add(info.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.Add(KpiAreaSnapshotMapper.MapToSnapshotRule(info, area?.Name));
|
||||
}
|
||||
}
|
||||
|
||||
PublishSnapshotNormalizer.NormalizeKpiAreas(merged);
|
||||
snapshot.PriceAreaInfos = merged;
|
||||
snapshot.PriceAreaRefs = [];
|
||||
if (areaIds.Count == 1)
|
||||
{
|
||||
snapshot.PriceAreaId = areaIds[0];
|
||||
snapshot.PriceAreaName = names.Count > 0 ? names[0] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.PriceAreaId = null;
|
||||
snapshot.PriceAreaName = names.Count > 0 ? string.Join("、", names) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||
ServiceChannelRulesPublishSnapshotDto snapshot,
|
||||
int? remoteAreaId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
snapshot.RemoteAreaRef = null;
|
||||
if (remoteAreaId is > 0)
|
||||
{
|
||||
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == remoteAreaId.Value, cancellationToken);
|
||||
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(remoteAreaId.Value, cancellationToken);
|
||||
var rules = infos.Select(i => KpiAreaSnapshotMapper.MapToSnapshotRule(i, area?.Name)).ToList();
|
||||
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||
snapshot.RemoteAreaId = remoteAreaId;
|
||||
snapshot.RemoteAreaName = area?.Name;
|
||||
snapshot.RemoteAreaInfos = rules;
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.RemoteAreaId = null;
|
||||
snapshot.RemoteAreaName = null;
|
||||
snapshot.RemoteAreaInfos = [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadLiveInfosIntoRulesSnapshotAsync(
|
||||
SalesProductRulesPublishSnapshotDto snapshot,
|
||||
int? blacklistAreaId,
|
||||
|
||||
@ -127,6 +127,96 @@ public static class PriceAreaSnapshotHelper
|
||||
return ids.Count > 0 ? ids[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道买价发布:从明细推断价区表并钉包(与产品卖价分区独立)。
|
||||
/// </summary>
|
||||
public static async Task ApplyChannelPriceAreaToSnapshotAsync(
|
||||
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||
IKpiAreaRepository kpiAreaRepository,
|
||||
KpiAreaPackService packService,
|
||||
string? publishUser = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var areaIds = await CollectResolvedChannelPriceAreaIdsAsync(
|
||||
snapshot,
|
||||
kpiAreaRepository,
|
||||
cancellationToken);
|
||||
snapshot.PriceAreaInfos = [];
|
||||
if (areaIds.Count == 0)
|
||||
{
|
||||
snapshot.PriceAreaId = null;
|
||||
snapshot.PriceAreaName = null;
|
||||
snapshot.PriceAreaRefs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var refs = new List<KpiAreaPackRefDto>();
|
||||
foreach (var areaId in areaIds)
|
||||
{
|
||||
refs.Add(await packService.EnsurePackAsync(
|
||||
areaId,
|
||||
KpiAreaPackService.AreaTypePrice,
|
||||
publishUser,
|
||||
description: "service-channel-price-publish",
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
snapshot.PriceAreaRefs = refs;
|
||||
if (refs.Count == 1)
|
||||
{
|
||||
snapshot.PriceAreaId = refs[0].AreaId;
|
||||
snapshot.PriceAreaName = refs[0].AreaName;
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.PriceAreaId = null;
|
||||
snapshot.PriceAreaName = string.Join(
|
||||
"、",
|
||||
refs.Select(x => x.AreaName).Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渠道买价 LIVE:不钉包,从当前库装 Infos。
|
||||
/// </summary>
|
||||
public static async Task ApplyLiveChannelPriceAreaInfosAsync(
|
||||
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||
IKpiAreaRepository kpiAreaRepository,
|
||||
KpiAreaPackService packService,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var areaIds = await CollectResolvedChannelPriceAreaIdsAsync(
|
||||
snapshot,
|
||||
kpiAreaRepository,
|
||||
cancellationToken);
|
||||
await packService.LoadLiveInfosIntoChannelPriceSnapshotAsync(snapshot, areaIds, cancellationToken);
|
||||
}
|
||||
|
||||
internal static async Task<IReadOnlyList<int>> CollectResolvedChannelPriceAreaIdsAsync(
|
||||
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||
IKpiAreaRepository kpiAreaRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new List<int>();
|
||||
var seen = new HashSet<int>();
|
||||
|
||||
void Add(int? areaId)
|
||||
{
|
||||
if (areaId is > 0 && seen.Add(areaId.Value))
|
||||
{
|
||||
result.Add(areaId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot);
|
||||
foreach (var zoneKey in zoneKeys)
|
||||
{
|
||||
Add(await TryResolveZoneKeyToAreaIdAsync(zoneKey, kpiAreaRepository, cancellationToken));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<int?> TryResolveZoneKeyToAreaIdAsync(
|
||||
string zoneKey,
|
||||
IKpiAreaRepository kpiAreaRepository,
|
||||
|
||||
@ -38,6 +38,7 @@ public static class PublishSnapshotNormalizer
|
||||
return;
|
||||
}
|
||||
|
||||
NormalizeKpiAreas(snapshot.PriceAreaInfos);
|
||||
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user