I am working on a project where I have to calculate the totals of a transaction. Unfortunately, coupons have proven to be quite an issue. There are four types of coupons: transaction percentage, transaction dollar amounts, item percentage and item dollar amounts.
I ended-up with this beast of a method, and was wondering if it is clear why I'm doing each part in the way I am. Are my comments are detailed enough? Is my code is more-or-less readable? Is there a way to simplify any of it?
private static Totals CalculateTotals(List<Item> items, List<Coupon> coupons, List<Payment> payments)
{
// Local vars, totals gets returned.
Totals totals = new Totals();
decimal subtotal = 0;
decimal workingSubtotal = 0;
decimal discounts = 0;
decimal tax = 0;
decimal paid = 0;
decimal taxRate = (Initialization.Location.TaxRate ?? 0);
// Get the subtotal before any discounts.
items.ForEach(i => subtotal += (i.Price ?? 0));
// An ItemCoupon takes a whole amount or percentage off of a single Item.
// It can take it off of the most expensive, or least. Nothing in the middle.
foreach (var coupon in coupons.OfType<ItemCoupon>())
{
// new Item to hold the item to be discounted.
Item item;
// Find which item to discount.
if (coupon.DiscountMostExpensive)
{
item = items.OrderByDescending(i => i.Price).FirstOrDefault();
}
else // Otherwise, Discount LEAST Expensive.
{
item = items.OrderByDescending(i => i.Price).LastOrDefault();
}
// Remove it from the list, before editing the price.
items.Remove(item);
// Set new price of item based on the type of coupon. (Percent, or whole dollar.)
if (coupon.PercentageCoupon)
{
item.Price = Utils.CalculatePercentage(item.Price, coupon.DiscountPercentage);
}
else
{
item.Price = (item.Price ?? 0) - (coupon.DiscountAmount ?? 0);
}
// Add the item back to the list, with the new price.
items.Add(item);
}
// Now that the single items have been discounted, let's get a wroking subtotal.
items.ForEach(i => workingSubtotal += (i.Price ?? 0));
// A TransactionCoupon takes a whole amount or percentage off of the entire transaction.
// To simplfy tax caculation--and because some items are non-taxable,
// oh and, because we don't want any one item going below zero--we
// split the discount over all the items, evenly.
foreach (var coupon in coupons.OfType<TransactionCoupon>())
{
if (coupon.PercentageCoupon)
{
// If it is a Percentage Coupon, simply take said percentage of off each item.
items.ForEach(i => i.Price = Utils.CalculatePercentage(i.Price, coupon.DiscountPercentage));
}
else
{
// If it is a whole amount, get each items percent of the of the subtotal, and discount them equally.
// This would look way too confusing using lambda.
foreach (var item in items)
{
decimal discount = (item.Price ?? 0) * ((coupon.DiscountAmount ?? 0) / workingSubtotal);
item.Price = item.Price - discount;
}
}
}
// Let's get the new-new-new subtotal.
workingSubtotal = 0;
items.ForEach(i => workingSubtotal += (i.Price ?? 0));
// Calculate the total discounts.
discounts += (subtotal - workingSubtotal);
// Set tax for order. (This must be done after ALL discounts have been applied)
foreach (var item in items.Where(i => i.Taxable))
{
tax += ((item.Price ?? 0) * taxRate);
}
// Get the total amount paid.
payments.ForEach(p => paid += p.Amount);
// Add all the results to the Totals struct.
totals.Subtotal = subtotal; // Never return the workingSubtotal;
totals.Discounts = discounts;
totals.Tax = tax;
totals.Paid = paid;
totals.Total = ((workingSubtotal + tax) - paid);
return totals;
}