admin/src/Oms.Application/Services/Orders/Issues/OrderFormulaFieldResolver.cs
zzh c8107cb565 feat: add additional-service billing, conditions, and fee lifecycle hardening
Support optional services and executable billing conditions on sell/buy prices, percent-of-value and percent-of-freight formulas, place-order/inbound payable on chargeable weight, outbound receivable freeze, channel remote areas, billing simulate, and insurance-aware claims.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 18:04:28 +08:00

322 lines
11 KiB
C#

using System.Globalization;
using System.Reflection;
using Oms.Application.Dtos.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Services.Orders.Issues;
public static class OrderFormulaFieldResolver
{
public static decimal GetNumericValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
{
var value = GetRawValue(fieldRef, context);
return ToDecimal(value);
}
public static bool StringContains(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context, string needle)
{
if (string.IsNullOrEmpty(needle))
{
return true;
}
if (fieldRef.Table == "t_order_declaration")
{
return context.Declarations.Any(d => ContainsValue(GetDeclarationValue(d, fieldRef.Field), needle))
|| context.CreateInput?.Items.Any(i => ContainsValue(GetDeclarationInputValue(i, fieldRef.Field), needle)) == true;
}
if (fieldRef.Table == "t_order_fee")
{
return context.Fees.Any(f => ContainsValue(GetPropertyValue(f, fieldRef.Field), needle));
}
var raw = GetRawValue(fieldRef, context);
return ContainsValue(raw, needle);
}
private static object? GetRawValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
{
if (fieldRef.Virtual)
{
return ResolveVirtual(fieldRef.Field, context);
}
return fieldRef.Table switch
{
"t_order" => ResolveOrderField(fieldRef.Field, context),
"t_order_recipient" => ResolveRecipientField(fieldRef.Field, context),
"t_order_sender" => ResolveSenderField(fieldRef.Field, context),
"t_order_declaration" => ResolveDeclarationField(fieldRef.Field, context),
"t_order_weight" => ResolveWeightField(fieldRef.Field, context),
"t_order_fee" => ResolveFeeField(fieldRef.Field, context),
"t_customer" => GetPropertyValue(context.Customer, fieldRef.Field),
_ => null
};
}
private static object? ResolveVirtual(string field, OrderRuleEvaluationContext context)
{
var (length, width, height) = GetPieceDimensions(context);
return field switch
{
"PieceMaxLength" => length,
"PieceMaxWidth" => width,
"PieceMaxHeight" => height,
"PieceSumLwh" => length + width + height,
_ => null
};
}
private static (decimal Length, decimal Width, decimal Height) GetPieceDimensions(OrderRuleEvaluationContext context)
{
var forecast = GetForecastWeight(context);
if (forecast != null)
{
return (forecast.Length ?? 0, forecast.Width ?? 0, forecast.Height ?? 0);
}
var input = context.CreateInput;
if (input != null)
{
return (input.Length ?? 0, input.Width ?? 0, input.Height ?? 0);
}
return (0, 0, 0);
}
private static OrderWeight? GetForecastWeight(OrderRuleEvaluationContext context) =>
context.Weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Forecast)
?? context.Weights.FirstOrDefault();
private static object? ResolveOrderField(string field, OrderRuleEvaluationContext context)
{
if (context.Order != null)
{
return GetPropertyValue(context.Order, field);
}
var input = context.CreateInput;
if (input == null)
{
return null;
}
return field switch
{
"RequestId" => input.RequestId,
"ClientNo" => input.ClientNo,
"TrackNo" => input.TrackNo,
"ServiceNo" => input.ServiceNo,
"ProductCode" => input.ProductCode,
"ChannelCode" => input.ChannelCode,
"ConsigneeCountry" => context.ConsigneeCountryCode,
"ConsigneeCountryName" => context.ConsigneeCountryName,
"CargoAttribute" => input.CargoAttribute,
"ExportCustomsMode" => input.ExportCustomsMode,
"WarehouseId" => input.WarehouseId,
"CustomerId" => input.CustomerId,
"IsCod" => input.IsCod ? (byte)1 : (byte)0,
"CodCurrency" => input.CodCurrency,
"CodSum" => input.CodSum,
"DeclaredCurrency" => input.DeclaredCurrency,
"DeclaredSum" => input.DeclaredSum,
"Quantity" => input.Quantity,
"IsRemote" => input.IsRemote,
"AdditionalServices" => SerializeAdditionalServices(input.AdditionalServices),
_ => null
};
}
private static string? SerializeAdditionalServices(List<string>? codes)
{
if (codes == null || codes.Count == 0)
{
return null;
}
var normalized = codes
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
return normalized.Count == 0
? null
: global::System.Text.Json.JsonSerializer.Serialize(normalized);
}
private static object? ResolveRecipientField(string field, OrderRuleEvaluationContext context)
{
if (context.Recipient != null)
{
return GetPropertyValue(context.Recipient, field);
}
var receiver = context.CreateInput?.Receiver;
if (receiver == null)
{
return null;
}
return field switch
{
"RecipientsName" => receiver.RecipientsName,
"CompanyName" => receiver.CompanyName,
"Phone" => receiver.Phone,
"TelePhone" => receiver.TelePhone,
"Email" => receiver.Email,
"Province" => receiver.Province,
"City" => receiver.City,
"StreetNumber" => receiver.StreetNumber,
"StreetAddress1" => receiver.StreetAddress1,
"StreetAddress2" => receiver.StreetAddress2,
"StreetAddress3" => receiver.StreetAddress3,
"ZipCode" => receiver.ZipCode,
"TaxType" => receiver.TaxType,
"TaxNo" => receiver.TaxNo,
"CertificateType" => receiver.CertificateType,
"CertificateNo" => receiver.CertificateNo,
"Eori" => receiver.Eori,
"Vat" => receiver.Vat,
_ => null
};
}
private static object? ResolveSenderField(string field, OrderRuleEvaluationContext context)
{
if (context.Sender != null)
{
return GetPropertyValue(context.Sender, field);
}
var sender = context.CreateInput?.Sender;
if (sender == null)
{
return null;
}
return field switch
{
"SenderName" => sender.SenderName,
"Company" => sender.Company,
"Phone" => sender.Phone,
"TelePhone" => sender.TelePhone,
"Email" => sender.Email,
"Province" => sender.Province,
"City" => sender.City,
"ZipCode" => sender.ZipCode,
"StreetAddress1" => sender.StreetAddress1,
"StreetAddress2" => sender.StreetAddress2,
"StreetAddress3" => sender.StreetAddress3,
"HouseNumber" => sender.HouseNumber,
_ => null
};
}
private static object? ResolveDeclarationField(string field, OrderRuleEvaluationContext context)
{
var declaration = context.Declarations.FirstOrDefault();
if (declaration != null)
{
return GetDeclarationValue(declaration, field);
}
var item = context.CreateInput?.Items.FirstOrDefault();
return item == null ? null : GetDeclarationInputValue(item, field);
}
private static object? ResolveWeightField(string field, OrderRuleEvaluationContext context)
{
var weight = GetForecastWeight(context);
if (weight != null)
{
return GetPropertyValue(weight, field);
}
var input = context.CreateInput;
if (input == null)
{
return null;
}
return field switch
{
"Length" => input.Length,
"Width" => input.Width,
"Height" => input.Height,
"ActualWeight" => input.ActualWeight,
"VolumetricWeight" => input.VolumetricWeight,
"ChargeableWeight" => input.ChargeableWeight,
"Cubic" => input.Cubic,
_ => null
};
}
private static object? ResolveFeeField(string field, OrderRuleEvaluationContext context)
{
var fee = context.Fees.FirstOrDefault();
return fee == null ? null : GetPropertyValue(fee, field);
}
private static object? GetDeclarationValue(OrderDeclaration declaration, string field) =>
field switch
{
"HsCode" => declaration.HsCode ?? declaration.ImHsCode ?? declaration.ExHsCode,
_ => GetPropertyValue(declaration, field)
};
private static object? GetDeclarationInputValue(OrderParcelItemInput item, string field) =>
field switch
{
"HsCode" => item.ImHsCode ?? item.ExHsCode,
"IsMagnetic" => item.IsMagnetic ? (byte)1 : (byte)0,
_ => GetPropertyValue(item, field)
};
private static object? GetPropertyValue(object? target, string fieldName)
{
if (target == null)
{
return null;
}
var prop = target.GetType().GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return prop?.GetValue(target);
}
private static decimal ToDecimal(object? value)
{
if (value == null)
{
return 0;
}
return value switch
{
bool b => b ? 1m : 0m,
byte bt => bt,
sbyte sb => sb,
short s => s,
ushort us => us,
int i => i,
uint ui => ui,
long l => l,
ulong ul => ul,
float f => (decimal)f,
double d => (decimal)d,
decimal m => m,
DateTime => 0,
Enum e => Convert.ToDecimal(e, CultureInfo.InvariantCulture),
_ => decimal.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed)
? parsed
: 0
};
}
private static bool ContainsValue(object? source, string needle) =>
!string.IsNullOrWhiteSpace(Convert.ToString(source, CultureInfo.InvariantCulture))
&& Convert.ToString(source, CultureInfo.InvariantCulture)!
.Contains(needle, StringComparison.OrdinalIgnoreCase);
}