Is there any class or extension methods that can help with calculating the actual amount of an order row? E.g. what a customer actually paid for a product, order row price minus product discounts for that row and the part of order discounts affecting that row.
The system must do this calculation for example on a partial shipment or partial return. Is that logic exposed somewhere so it can be used from somewhere else?
Litium.Sales.SalesReturnOrderBuilder is doing the calculation internally, and uses internal logic to find the refund value. you can use service decorator to change what is generated.
Essentially the amounts will get calculated based on what should be refunded.
If you are looking for a method that can effectively calculate what the actual price after discounts, you can use the following. It calculates the discount allocated per payment row, given the whole payment object. (note that, this considers the ArticleNumber as the field linking a direct product discount with a order row, if you are using the api, you can actually get the discountInfo which shows the relationship between discount rows and product rows at a finer level).
private Dictionary<PaymentRow, double> ApportionDiscounts(Payment payment)
{
try
{
//all products rows in a dictionary with dictionary value being total apportioned discount.
var productRows = payment.Rows.Where(x => x.Type == PaymentRowType.Product).ToDictionary(r => r, _ => 0.0);
//all discount rows in a dictionary with dictionary value being the totalIncludingVAT (with a positive value)
//As we allocate this discount, we will be reducing this value so we know whether the discount got fully allocated or not.
var discountRows = payment.Rows.Where(x => x.Type == PaymentRowType.Discount).ToDictionary(r => r, r => r.TotalIncludingVat);
foreach (var discountRow in discountRows.Keys.ToList())
{
if (discountRow.ArticleNumber is null)
{
continue;
}
//for this discount row, we find all the target product rows where article numbers match.
//then we pro-rata distribute the current discount row among those target product rows.
var targetProductRows = productRows.Keys.Where(x => x.ArticleNumber == discountRow.ArticleNumber).ToList();
if (targetProductRows.Count == 0)
{
//discount rows article number is not null, but it is not pointing to a product row.
//therefore, this discount cannot be re-distributed, so we remove it from collection.
discountRows.Remove(discountRow); // (comment out if you want to redistribute this also as order level-discounts)
continue; // (comment out the discountRows.Remove(discountRow) to redistribute this row among all product rows later.
}
double targetRowsSum = targetProductRows.Sum(x => x.TotalIncludingVat);
if (targetRowsSum == 0)
{
//avoid div by zero, happens if unit price zero products are present.
//we will re-distribute this row among all product rows later.
continue;
}
foreach (var targetProductRow in targetProductRows)
{
double partOfDiscount = Math.Round((targetProductRow.TotalIncludingVat / targetRowsSum) * discountRow.TotalIncludingVat, 2);
var totalDiscounts = productRows[targetProductRow] + partOfDiscount;
if (targetProductRow.TotalIncludingVat >= Math.Abs(totalDiscounts)) //Makesure the amount of discounts allocated is not more than actual row total. note: discounts is a negative value
{
discountRows[discountRow] -= partOfDiscount;
productRows[targetProductRow] += partOfDiscount;
}
else
{
//It would be confusing if only part of the discount got allocated because this is supposed to be a full discount.
//Ideally code should not hit here, but if this happens, let the discount be considered as a order level discount and let it get distributed as a order level discount.
//so nothing to do here.
}
}
}
//redistribute the total remaining discounts among all the product rows.
//this should now be done proportionate to the remaining order row total after product discounts, to avoid attracting more discounts than the row can handle.
var discountSumToReDistribute = discountRows.Values.Sum();
var productRemainings = productRows.ToDictionary(r => r.Key, r => r.Key.TotalIncludingVat + r.Value);
var denom = productRemainings.Values.Sum();
//we cannot redistribute more than what is remaining in row totals
if (denom >= Math.Abs(discountSumToReDistribute))
{
foreach (var kvp in productRemainings)
{
var partOfDiscount = Math.Round(discountSumToReDistribute * (kvp.Value / denom), 2);
productRows[kvp.Key] += partOfDiscount;
}
}
else
{
//this point should not be possible normally. But rounding off differences and can have an effect.
foreach (var kvp in productRemainings)
{
productRows[kvp.Key] = kvp.Key.TotalIncludingVat * -1; //just makes the whole row to be zero by assigning the rowTotal
}
}
return productRows;
}
catch (Exception ex)
{
_logger.LogError(ex, "Distributing discounts to order rows for ingrid metadata failed. Message {Message}", ex.Message);
return [];
}
}
Using SalesReturnOrderBuilder sounds like a way to go. But can you use that without actually creating a return? In our case we need to actual paid amount for a product when ReadyToShip happens so the order isn’t completed yet.
Do you have any other ideas on how to do this? It would be great to use your internal logic in some way.
We could try the code you sent but from what I can see it will not work if you have more than one payment, like when doing partial shipments, so we need to change it a bit to be able to use it. It would be better not to manage this logic our selves and instead use your logic if you decide to change something.